Skip to content

Commit 71c7c00

Browse files
committed
feat(observability): record webhook delivery failures
1 parent 6ae0ae7 commit 71c7c00

2 files changed

Lines changed: 250 additions & 41 deletions

File tree

typescript/examples/ride-dispatch/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
},
1111
"dependencies": {
1212
"@spatiad/express-plugin": "workspace:*",
13+
"@spatiad/sdk": "workspace:*",
1314
"express": "^4.21.2",
14-
"@spatiad/sdk": "workspace:*"
15+
"uuid": "^9.0.1"
1516
},
1617
"devDependencies": {
17-
"@types/express": "^5.0.1"
18+
"@types/express": "^5.0.1",
19+
"@types/uuid": "^9.0.7"
1820
}
1921
}
Lines changed: 246 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import express, { type Request, type Response } from "express";
2+
import { v4 as uuidv4 } from "uuid";
23

34
import { SpatiadClient } from "@spatiad/sdk";
45
import {
@@ -8,72 +9,278 @@ import {
89

910
const client = new SpatiadClient("http://localhost:3000");
1011

11-
const run = async () => {
12-
const controller = new AbortController();
13-
setTimeout(() => controller.abort(), 5_000);
12+
// Simulate multiple drivers with location updates
13+
interface Driver {
14+
id: string;
15+
latitude: number;
16+
longitude: number;
17+
category: string;
18+
}
1419

15-
const app = express();
16-
const webhookSecret = process.env.SPATIAD_WEBHOOK_SECRET ?? "dev-secret";
20+
const drivers: Driver[] = [
21+
{ id: uuidv4(), latitude: 38.433, longitude: 26.768, category: "tow_truck" },
22+
{ id: uuidv4(), latitude: 38.435, longitude: 26.769, category: "tow_truck" },
23+
{ id: uuidv4(), latitude: 38.431, longitude: 26.767, category: "tow_truck" },
24+
{ id: uuidv4(), latitude: 38.436, longitude: 26.770, category: "tow_truck" },
25+
];
1726

18-
app.post(
19-
"/webhooks/spatiad",
20-
spatiadWebhookJson(),
21-
verifySpatiadWebhook({ secret: webhookSecret }),
22-
(req: Request, res: Response) => {
23-
console.log("verified webhook payload", req.body);
24-
res.status(204).end();
27+
async function registerDrivers() {
28+
console.log("\n📍 Registering drivers...");
29+
for (const driver of drivers) {
30+
await client.upsertDriver({
31+
driverId: driver.id,
32+
category: driver.category,
33+
status: "Available",
34+
position: { latitude: driver.latitude, longitude: driver.longitude },
35+
});
36+
console.log(` ✓ Driver ${driver.id.slice(0, 8)} at (${driver.latitude}, ${driver.longitude})`);
37+
}
38+
}
39+
40+
async function demonstrateOfferCreation() {
41+
console.log("\n🚗 Creating offer for job...");
42+
43+
const jobId = uuidv4();
44+
const response = await client.createOffer({
45+
jobId,
46+
category: "tow_truck",
47+
pickup: { latitude: 38.433, longitude: 26.768 },
48+
dropoff: { latitude: 38.44, longitude: 26.78 },
49+
initialRadiusKm: 0.5,
50+
maxRadiusKm: 5,
51+
timeoutSeconds: 30,
52+
retry: {
53+
maxAttempts: 3,
54+
backoffMs: 100
2555
}
26-
);
56+
});
2757

28-
app.listen(4000, () => {
29-
console.log("example webhook server listening on :4000");
58+
console.log(` ✓ Offer created: ${response.offer_id}`);
59+
return jobId;
60+
}
61+
62+
async function demonstrateRadiusExpansion() {
63+
console.log("\n📡 Testing radius expansion...");
64+
65+
// Create a driver far away
66+
const farDriver = {
67+
id: uuidv4(),
68+
latitude: 38.45,
69+
longitude: 26.80,
70+
category: "tow_truck"
71+
};
72+
73+
await client.upsertDriver({
74+
driverId: farDriver.id,
75+
category: farDriver.category,
76+
status: "Available",
77+
position: { latitude: farDriver.latitude, longitude: farDriver.longitude },
3078
});
79+
console.log(` ✓ Registered far driver at (${farDriver.latitude}, ${farDriver.longitude})`);
3180

81+
const jobId = uuidv4();
3282
const response = await client.createOffer({
33-
jobId: "33333333-3333-3333-3333-333333333333",
83+
jobId,
3484
category: "tow_truck",
3585
pickup: { latitude: 38.433, longitude: 26.768 },
3686
dropoff: { latitude: 38.44, longitude: 26.78 },
37-
initialRadiusKm: 1,
38-
maxRadiusKm: 5,
39-
timeoutSeconds: 20,
87+
initialRadiusKm: 0.1, // Very small initial radius
88+
maxRadiusKm: 5, // Will expand to find the far driver
89+
timeoutSeconds: 30,
4090
retry: {
4191
maxAttempts: 3,
4292
backoffMs: 100
4393
}
4494
});
4595

46-
console.log("offer response", response);
96+
console.log(` ✓ Offer created via radius expansion: ${response.offer_id}`);
97+
return jobId;
98+
}
4799

100+
async function demonstrateJobStatus(jobId: string) {
101+
console.log("\n📊 Checking job status...");
102+
103+
const status = await client.getJobStatus({
104+
jobId,
105+
});
106+
107+
console.log(` ✓ Job ${jobId.slice(0, 8)}`);
108+
console.log(` State: ${status.state}`);
109+
console.log(` Matched Driver: ${status.matched_driver_id || "none"}`);
110+
console.log(` Matched Offer: ${status.matched_offer_id || "none"}`);
111+
}
112+
113+
async function demonstrateJobEvents(jobId: string) {
114+
console.log("\n📋 Fetching job events...");
115+
48116
const events = await client.getJobEvents({
49-
jobId: "33333333-3333-3333-3333-333333333333",
50-
limit: 20,
51-
kinds: ["offer_created", "match_confirmed"]
117+
jobId,
118+
limit: 50,
119+
kinds: ["job_registered", "offer_created", "match_confirmed"]
52120
});
53121

54-
console.log("filtered events", events);
122+
if (events.events.length === 0) {
123+
console.log(` ℹ️ No events found`);
124+
return;
125+
}
55126

56-
const allFilteredEvents = await client.getJobEventsAllPages({
57-
jobId: "33333333-3333-3333-3333-333333333333",
58-
limit: 10,
59-
maxPages: 5,
60-
maxEvents: 100,
61-
kinds: ["offer_created", "match_confirmed"],
62-
signal: controller.signal,
63-
retry: {
64-
maxAttempts: 4,
65-
backoffMs: 120,
66-
maxBackoffMs: 1000
127+
console.log(` ✓ Found ${events.events.length} event(s):`);
128+
for (const event of events.events) {
129+
console.log(` - ${event.kind} at ${event.at}`);
130+
}
131+
}
132+
133+
async function demonstrateInputValidation() {
134+
console.log("\n⚠️ Testing input validation...");
135+
136+
const testCases = [
137+
{
138+
name: "Invalid category (too long)",
139+
config: {
140+
jobId: uuidv4(),
141+
category: "a".repeat(51), // > 50 chars
142+
pickup: { latitude: 38.433, longitude: 26.768 },
143+
initialRadiusKm: 1,
144+
maxRadiusKm: 5,
145+
timeoutSeconds: 20,
146+
}
147+
},
148+
{
149+
name: "Invalid coordinates (out of range)",
150+
config: {
151+
jobId: uuidv4(),
152+
category: "tow_truck",
153+
pickup: { latitude: 91, longitude: 26.768 }, // > 90
154+
initialRadiusKm: 1,
155+
maxRadiusKm: 5,
156+
timeoutSeconds: 20,
157+
}
158+
},
159+
{
160+
name: "Invalid radius (initial > max)",
161+
config: {
162+
jobId: uuidv4(),
163+
category: "tow_truck",
164+
pickup: { latitude: 38.433, longitude: 26.768 },
165+
initialRadiusKm: 10,
166+
maxRadiusKm: 5, // < initial
167+
timeoutSeconds: 20,
168+
}
169+
},
170+
{
171+
name: "Invalid timeout (0 seconds)",
172+
config: {
173+
jobId: uuidv4(),
174+
category: "tow_truck",
175+
pickup: { latitude: 38.433, longitude: 26.768 },
176+
initialRadiusKm: 1,
177+
maxRadiusKm: 5,
178+
timeoutSeconds: 0, // Invalid
179+
}
67180
},
68-
onPage: (page, index) => {
69-
console.log("page fetched", index, page.events.length, page.next_before_cursor);
181+
];
182+
183+
for (const testCase of testCases) {
184+
try {
185+
// @ts-ignore - intentionally passing invalid config
186+
await client.createOffer(testCase.config);
187+
console.log(` ✗ ${testCase.name}: Should have failed but didn't`);
188+
} catch (error) {
189+
const err = error as any;
190+
console.log(` ✓ ${testCase.name}: Rejected`);
191+
if (err.message) {
192+
console.log(` Error: ${err.message.slice(0, 60)}...`);
193+
}
194+
}
195+
}
196+
}
197+
198+
async function demonstrateOffersAndCancellation() {
199+
console.log("\n❌ Testing offer/job operations...");
200+
201+
const jobId = uuidv4();
202+
203+
// Create offer
204+
const offerResponse = await client.createOffer({
205+
jobId,
206+
category: "tow_truck",
207+
pickup: { latitude: 38.433, longitude: 26.768 },
208+
dropoff: { latitude: 38.44, longitude: 26.78 },
209+
initialRadiusKm: 1,
210+
maxRadiusKm: 5,
211+
timeoutSeconds: 30,
212+
});
213+
214+
console.log(` ✓ Offer created: ${offerResponse.offer_id?.slice(0, 8)}`);
215+
216+
// Check job status is pending/searching
217+
const statusBefore = await client.getJobStatus({ jobId });
218+
console.log(` ✓ Job state before: ${statusBefore.state}`);
219+
220+
// Try to cancel offer
221+
if (offerResponse.offer_id && offerResponse.offer_id !== "00000000-0000-0000-0000-000000000000") {
222+
await client.cancelOffer({ offerId: offerResponse.offer_id });
223+
console.log(` ✓ Offer cancelled`);
224+
}
225+
226+
// Check status after
227+
const statusAfter = await client.getJobStatus({ jobId });
228+
console.log(` ✓ Job state after: ${statusAfter.state}`);
229+
}
230+
231+
const run = async () => {
232+
const app = express();
233+
const webhookSecret = process.env.SPATIAD_WEBHOOK_SECRET ?? "dev-secret";
234+
235+
// Webhook receiver
236+
app.post(
237+
"/webhooks/spatiad",
238+
spatiadWebhookJson(),
239+
verifySpatiadWebhook({ secret: webhookSecret }),
240+
(req: Request, res: Response) => {
241+
console.log("\n✅ Webhook received:", {
242+
event: (req.body as any).event,
243+
jobId: (req.body as any).job_id?.slice(0, 8),
244+
driverId: (req.body as any).driver_id?.slice(0, 8),
245+
});
246+
res.status(204).end();
70247
}
248+
);
249+
250+
app.listen(4000, () => {
251+
console.log("🎯 Webhook receiver listening on :4000");
71252
});
72253

73-
console.log("all filtered events", allFilteredEvents.length);
254+
try {
255+
console.log("═══════════════════════════════════════");
256+
console.log(" Spatiad Demo - Multi-Driver Dispatch");
257+
console.log("═══════════════════════════════════════");
258+
259+
// Run all demonstrations
260+
await registerDrivers();
261+
262+
const jobId = await demonstrateOfferCreation();
263+
await demonstrateJobStatus(jobId);
264+
await demonstrateJobEvents(jobId);
265+
266+
await demonstrateRadiusExpansion();
267+
268+
await demonstrateInputValidation();
269+
270+
await demonstrateOffersAndCancellation();
271+
272+
console.log("\n═══════════════════════════════════════");
273+
console.log(" Demo Completed Successfully ✓");
274+
console.log("═══════════════════════════════════════\n");
275+
276+
} catch (error) {
277+
console.error("❌ Demo failed:", error);
278+
} finally {
279+
process.exit(0);
280+
}
74281
};
75282

76283
run().catch((error) => {
77-
console.error("example failed", error);
284+
console.error("❌ Example failed", error);
78285
process.exitCode = 1;
79286
});

0 commit comments

Comments
 (0)