convex-tracer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +592 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/helpers.d.ts +31 -0
- package/dist/client/helpers.d.ts.map +1 -0
- package/dist/client/helpers.js +177 -0
- package/dist/client/helpers.js.map +1 -0
- package/dist/client/index.d.ts +210 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +355 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/tracer-api/index.d.ts +27 -0
- package/dist/client/tracer-api/index.d.ts.map +1 -0
- package/dist/client/tracer-api/index.js +177 -0
- package/dist/client/tracer-api/index.js.map +1 -0
- package/dist/client/tracer-api/types.d.ts +143 -0
- package/dist/client/tracer-api/types.d.ts.map +1 -0
- package/dist/client/tracer-api/types.js +2 -0
- package/dist/client/tracer-api/types.js.map +1 -0
- package/dist/client/types.d.ts +168 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +139 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +161 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +349 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +75 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +46 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/types.d.ts +286 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component/types.js +28 -0
- package/dist/component/types.js.map +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/types.d.ts +8 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/types.js +2 -0
- package/dist/react/types.js.map +1 -0
- package/package.json +121 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/helpers.ts +278 -0
- package/src/client/index.ts +593 -0
- package/src/client/setup.test.ts +26 -0
- package/src/client/tracer-api/index.ts +235 -0
- package/src/client/tracer-api/types.ts +168 -0
- package/src/client/types.ts +257 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +199 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.ts +399 -0
- package/src/component/schema.ts +62 -0
- package/src/component/setup.test.ts +11 -0
- package/src/component/types.ts +38 -0
- package/src/react/index.ts +36 -0
- package/src/react/types.ts +15 -0
- package/src/test.ts +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# Convex Tracer
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/convex-tracer)
|
|
4
|
+
|
|
5
|
+
<!-- START: Include on https://convex.dev/components -->
|
|
6
|
+
|
|
7
|
+
**Powerful Observability and tracing for Convex applications.** Track function
|
|
8
|
+
calls across queries, mutations, and actions with detailed insights, nested
|
|
9
|
+
spans, and automatic error tracking.
|
|
10
|
+
|
|
11
|
+
## Why use Convex Tracer?
|
|
12
|
+
|
|
13
|
+
- **Deep Visibility**: See exactly how your Convex functions execute, including
|
|
14
|
+
nested calls and cross-function traces
|
|
15
|
+
- **Debug Production Issues**: Preserve error traces and have a complete view of
|
|
16
|
+
of what went wrong, were it went wrong and why
|
|
17
|
+
- **Trace Sampling**: Control costs with configurable sample rates while
|
|
18
|
+
preserving important traces
|
|
19
|
+
- **Zero Boilerplate**: Simple wrapper functions that feel natural in Convex
|
|
20
|
+
|
|
21
|
+
Perfect for complex workflows like multi-step order processing, payment flows,
|
|
22
|
+
or any scenario where you need to understand what's happening across multiple
|
|
23
|
+
function calls.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the component:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install convex-tracer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Create a `convex.config.ts` file in your app's `convex/` folder and install the
|
|
34
|
+
component:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// convex/convex.config.ts
|
|
38
|
+
import { defineApp } from "convex/server";
|
|
39
|
+
import tracer from "convex-tracer/convex.config";
|
|
40
|
+
|
|
41
|
+
const app = defineApp();
|
|
42
|
+
app.use(tracer);
|
|
43
|
+
|
|
44
|
+
export default app;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
Create a tracer instance in your Convex backend:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// convex/tracer.ts
|
|
53
|
+
import { Tracer } from "convex-tracer";
|
|
54
|
+
import { components } from "./_generated/api";
|
|
55
|
+
import { DataModel } from "./_generated/dataModel";
|
|
56
|
+
|
|
57
|
+
export const {
|
|
58
|
+
tracedQuery,
|
|
59
|
+
tracedMutation,
|
|
60
|
+
tracedAction,
|
|
61
|
+
internalTracedQuery,
|
|
62
|
+
internalTracedMutation,
|
|
63
|
+
internalTracedAction,
|
|
64
|
+
tracer,
|
|
65
|
+
} = new Tracer<DataModel>(
|
|
66
|
+
components.tracer,
|
|
67
|
+
// Default options
|
|
68
|
+
{
|
|
69
|
+
sampleRate: 0.1, // Sample 10% of traces
|
|
70
|
+
preserveErrors: true, // Always keep error traces
|
|
71
|
+
retentionMinutes: 120, // Keep traces for 2 hours
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use traced functions just like regular Convex functions:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// convex/shop.ts
|
|
80
|
+
import { v } from "convex/values";
|
|
81
|
+
import { tracedMutation } from "./tracer";
|
|
82
|
+
|
|
83
|
+
export const createOrder = tracedMutation({
|
|
84
|
+
name: "createOrder",
|
|
85
|
+
args: {
|
|
86
|
+
customerId: v.id("customers"),
|
|
87
|
+
items: v.array(
|
|
88
|
+
v.object({
|
|
89
|
+
productId: v.id("products"),
|
|
90
|
+
quantity: v.number(),
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
handler: async (ctx, args) => {
|
|
95
|
+
await ctx.tracer.info("Starting order creation", {
|
|
96
|
+
customerId: args.customerId,
|
|
97
|
+
itemCount: args.items.length,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Your business logic here
|
|
101
|
+
const orderId = await ctx.db.insert("orders", {
|
|
102
|
+
customerId: args.customerId,
|
|
103
|
+
items: args.items,
|
|
104
|
+
status: "pending",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await ctx.tracer.info("Order created successfully", { orderId });
|
|
108
|
+
|
|
109
|
+
return orderId;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Core Features
|
|
115
|
+
|
|
116
|
+
### Nested Spans
|
|
117
|
+
|
|
118
|
+
Create detailed traces with nested operations:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export const processPayment = tracedMutation({
|
|
122
|
+
name: "processPayment",
|
|
123
|
+
args: { orderId: v.id("orders"), amount: v.number() },
|
|
124
|
+
handler: async (ctx, { orderId, amount }) => {
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
const reservation = await ctx.tracer.withSpan(
|
|
127
|
+
`reserveItem_${item.productId}`,
|
|
128
|
+
async (span) => {
|
|
129
|
+
await span.updateMetadata({
|
|
130
|
+
productId: item.productId,
|
|
131
|
+
requestedQty: item.quantity,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const inventory = await ctx.db
|
|
135
|
+
.query("inventory")
|
|
136
|
+
.withIndex("by_product", (q) => q.eq("productId", item.productId))
|
|
137
|
+
.first();
|
|
138
|
+
|
|
139
|
+
// More logic here
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Cross-Function Tracing
|
|
150
|
+
|
|
151
|
+
Automatically trace calls across multiple functions:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
export const createOrder = tracedMutation({
|
|
155
|
+
name: "createOrder",
|
|
156
|
+
args: { customerId: v.id("customers"), items: v.array(...) },
|
|
157
|
+
handler: async (ctx, args) => {
|
|
158
|
+
// This call is automatically traced as part of the same trace
|
|
159
|
+
const validation = await ctx.runTracedMutation(
|
|
160
|
+
internal.shop.validateCustomer,
|
|
161
|
+
{ customerId: args.customerId }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Process payment - also traced
|
|
165
|
+
const payment = await ctx.runTracedMutation(
|
|
166
|
+
internal.shop.processPayment,
|
|
167
|
+
{ orderId, amount: total }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return { orderId, status: "confirmed" };
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Lifecycle Hooks
|
|
176
|
+
|
|
177
|
+
Control trace behavior with lifecycle callbacks:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
export const getProductWithInventory = tracedQuery({
|
|
181
|
+
name: "getProductWithInventory",
|
|
182
|
+
args: { productId: v.id("products") },
|
|
183
|
+
onSuccess: async (ctx, args, result) => {
|
|
184
|
+
if (result.inventory < 10) {
|
|
185
|
+
await ctx.tracer.warn("Low inventory detected", {
|
|
186
|
+
productId: args.productId,
|
|
187
|
+
inventory: result.inventory,
|
|
188
|
+
});
|
|
189
|
+
await ctx.tracer.preserve(); // Keep this trace!
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onError: async (ctx, args, error) => {
|
|
193
|
+
await ctx.tracer.error("Product fetch failed", {
|
|
194
|
+
productId: args.productId,
|
|
195
|
+
error: error.message,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
handler: async (ctx, { productId }) => {
|
|
199
|
+
const product = await ctx.db.get(productId);
|
|
200
|
+
const inventory = await checkInventory(ctx, productId);
|
|
201
|
+
return { ...product, inventory };
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Logging and Metadata
|
|
207
|
+
|
|
208
|
+
Rich logging at different severity levels:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
await ctx.tracer.info("Processing step complete", { step: 1 });
|
|
212
|
+
await ctx.tracer.warn("Approaching rate limit", { remaining: 10 });
|
|
213
|
+
await ctx.tracer.error("Validation failed", { reason: "INVALID_EMAIL" });
|
|
214
|
+
|
|
215
|
+
// Add metadata to the current span
|
|
216
|
+
await ctx.tracer.updateMetadata({
|
|
217
|
+
userId: user._id,
|
|
218
|
+
planType: "premium",
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Configuration Options
|
|
223
|
+
|
|
224
|
+
### Global Configuration
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
new Tracer<DataModel>(components.tracer, {
|
|
228
|
+
sampleRate: 0.1, // Sample 10% of traces (0.0-1.0)
|
|
229
|
+
preserveErrors: true, // Always preserve error traces
|
|
230
|
+
retentionMinutes: 120, // Keep traces for 2 hours
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Per-Function Configuration
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
export const myFunction = tracedMutation({
|
|
238
|
+
name: "myFunction",
|
|
239
|
+
args: { userId: v.id("users") },
|
|
240
|
+
|
|
241
|
+
// Tracing options
|
|
242
|
+
sampleRate: 1.0, // Override: trace 100% of calls
|
|
243
|
+
logArgs: ["userId"], // Log specific arguments or all with "true"
|
|
244
|
+
logReturn: true, // Log the return value
|
|
245
|
+
|
|
246
|
+
// Lifecycle hooks
|
|
247
|
+
onStart: async (ctx, args) => {
|
|
248
|
+
await ctx.tracer.info("Function will start");
|
|
249
|
+
},
|
|
250
|
+
onSuccess: async (ctx, args, result) => {
|
|
251
|
+
await ctx.tracer.info("Function succeeded", { result });
|
|
252
|
+
},
|
|
253
|
+
onError: async (ctx, args, error) => {
|
|
254
|
+
await ctx.tracer.error("Function failed", { error: error.message });
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
handler: async (ctx, args) => {
|
|
258
|
+
// Your logic here
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## API Reference
|
|
264
|
+
|
|
265
|
+
### Tracer Context Methods
|
|
266
|
+
|
|
267
|
+
All traced functions receive an enhanced context with these methods:
|
|
268
|
+
|
|
269
|
+
#### `ctx.tracer.info(message, metadata?)`
|
|
270
|
+
|
|
271
|
+
Log an info-level message with optional metadata.
|
|
272
|
+
|
|
273
|
+
#### `ctx.tracer.warn(message, metadata?)`
|
|
274
|
+
|
|
275
|
+
Log a warning-level message.
|
|
276
|
+
|
|
277
|
+
#### `ctx.tracer.error(message, metadata?)`
|
|
278
|
+
|
|
279
|
+
Log an error-level message.
|
|
280
|
+
|
|
281
|
+
#### `ctx.tracer.updateMetadata(metadata)`
|
|
282
|
+
|
|
283
|
+
Add metadata to the current span.
|
|
284
|
+
|
|
285
|
+
#### `ctx.tracer.preserve()`
|
|
286
|
+
|
|
287
|
+
Mark this trace to be preserved regardless of sample rate.
|
|
288
|
+
|
|
289
|
+
#### `ctx.tracer.discard()`
|
|
290
|
+
|
|
291
|
+
Discard this trace.
|
|
292
|
+
|
|
293
|
+
#### `ctx.tracer.sample(sampleRate?)`
|
|
294
|
+
|
|
295
|
+
Sample this trace with an optional override for the sample rate.
|
|
296
|
+
|
|
297
|
+
#### `ctx.tracer.withSpan(name, callback)`
|
|
298
|
+
|
|
299
|
+
Create a nested span for a block of code:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const result = await ctx.tracer.withSpan("spanName", async (span) => {
|
|
303
|
+
await span.info("Inside nested span");
|
|
304
|
+
await span.updateMetadata({ key: "value" });
|
|
305
|
+
return someValue;
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
##### `span.info(message, metadata?)`
|
|
310
|
+
|
|
311
|
+
Log an info-level message with optional metadata.
|
|
312
|
+
|
|
313
|
+
##### `span.warn(message, metadata?)`
|
|
314
|
+
|
|
315
|
+
Log a warning-level message.
|
|
316
|
+
|
|
317
|
+
##### `span.error(message, metadata?)`
|
|
318
|
+
|
|
319
|
+
Log an error-level message.
|
|
320
|
+
|
|
321
|
+
##### `span.updateMetadata(metadata)`
|
|
322
|
+
|
|
323
|
+
Add metadata to the current span.
|
|
324
|
+
|
|
325
|
+
##### `span.withSpan(name, callback)`
|
|
326
|
+
|
|
327
|
+
Create a nested span for a block of code:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
const result = await span.withSpan("createPost", async (span) => {
|
|
331
|
+
await span.info("Creating post");
|
|
332
|
+
return await ctx.db.insert("posts", args);
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### `ctx.runTracedQuery(funcRef, args)`
|
|
337
|
+
|
|
338
|
+
Call another traced query while maintaining the trace context.
|
|
339
|
+
|
|
340
|
+
#### `ctx.runTracedMutation(funcRef, args)`
|
|
341
|
+
|
|
342
|
+
Call another traced mutation while maintaining the trace context.
|
|
343
|
+
|
|
344
|
+
#### `ctx.runTracedAction(funcRef, args)`
|
|
345
|
+
|
|
346
|
+
Call another traced action while maintaining the trace context (actions only).
|
|
347
|
+
|
|
348
|
+
### Retrieving Traces
|
|
349
|
+
|
|
350
|
+
Query traces from your frontend or other functions:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// In your convex functions
|
|
354
|
+
import { tracer } from "./tracer";
|
|
355
|
+
|
|
356
|
+
export const getTrace = query({
|
|
357
|
+
args: { traceId: v.string() },
|
|
358
|
+
handler: async (ctx, args) => {
|
|
359
|
+
return await tracer.tracer.getTrace(ctx, args.traceId);
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
export const listTraces = query({
|
|
364
|
+
args: {
|
|
365
|
+
status: v.optional(v.union(v.literal("success"), v.literal("error"))),
|
|
366
|
+
limit: v.optional(v.number()),
|
|
367
|
+
},
|
|
368
|
+
handler: async (ctx, args) => {
|
|
369
|
+
return await tracer.tracer.listTraces(ctx, args);
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### React Hooks
|
|
375
|
+
|
|
376
|
+
Use traced functions in your React components:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { useTracedMutation } from "convex-tracer/react";
|
|
380
|
+
import { api } from "../convex/_generated/api";
|
|
381
|
+
|
|
382
|
+
function MyComponent() {
|
|
383
|
+
const createOrder = useTracedMutation(api.shop.createOrder);
|
|
384
|
+
|
|
385
|
+
const handleOrder = async () => {
|
|
386
|
+
const result = await createOrder({
|
|
387
|
+
customerId: "...",
|
|
388
|
+
items: [...]
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (result.success) {
|
|
392
|
+
console.log("Order created:", result.data);
|
|
393
|
+
} else {
|
|
394
|
+
console.error("Order failed:", result.error);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return <button onClick={handleOrder}>Create Order</button>;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Advanced Patterns
|
|
403
|
+
|
|
404
|
+
### Multi-Step Workflows
|
|
405
|
+
|
|
406
|
+
Trace complex workflows across multiple functions:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
export const processOrder = tracedMutation({
|
|
410
|
+
name: "processOrder",
|
|
411
|
+
handler: async (ctx, args) => {
|
|
412
|
+
// Step 1: Validate customer
|
|
413
|
+
const validation = await ctx.runTracedQuery(internal.validateCustomer, {
|
|
414
|
+
customerId: args.customerId,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Step 2: Reserve inventory
|
|
418
|
+
await ctx.runTracedMutation(internal.reserveInventory, {
|
|
419
|
+
items: args.items,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Step 3: Process payment
|
|
423
|
+
await ctx.runTracedMutation(internal.processPayment, { amount: total });
|
|
424
|
+
|
|
425
|
+
// Step 4: Send notifications (async)
|
|
426
|
+
await ctx.scheduler.runAfter(0, api.sendNotification, {
|
|
427
|
+
orderId,
|
|
428
|
+
__traceContext: {
|
|
429
|
+
traceId: ctx.tracer.getTraceId(),
|
|
430
|
+
spanId: ctx.tracer.getSpanId(),
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Conditional Preservation
|
|
438
|
+
|
|
439
|
+
Preserve traces based on business logic:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
export const placeOrder = tracedMutation({
|
|
443
|
+
name: "placeOrder",
|
|
444
|
+
onSuccess: async (ctx, args, result) => {
|
|
445
|
+
// Preserve high-value orders
|
|
446
|
+
if (result.total > 1000) {
|
|
447
|
+
await ctx.tracer.preserve();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Preserve orders from VIP customers
|
|
451
|
+
const customer = await ctx.db.get(args.customerId);
|
|
452
|
+
if (customer.vipStatus) {
|
|
453
|
+
await ctx.tracer.preserve();
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
handler: async (ctx, args) => {
|
|
457
|
+
// Process order...
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Examples
|
|
463
|
+
|
|
464
|
+
See more detailed examples in `example/convex/shop.ts`
|
|
465
|
+
|
|
466
|
+
Found a bug? Feature request?
|
|
467
|
+
[File it here](https://github.com/Moumen-io/convex-tracer/issues).
|
|
468
|
+
|
|
469
|
+
## Best Practices
|
|
470
|
+
|
|
471
|
+
1. **Use descriptive span names**: `"validatePaymentMethod"` not `"step1"`
|
|
472
|
+
2. **Add relevant metadata**: Include IDs, counts, and business-relevant data
|
|
473
|
+
3. **Preserve strategically**: Don't preserve everything. Focus on errors and
|
|
474
|
+
edge cases
|
|
475
|
+
4. **Sample appropriately**: Use low sample rates in production (0.05-0.15)
|
|
476
|
+
5. **Log at the right level**: `info` for normal flow, `warn` for concerning but
|
|
477
|
+
handled issues, `error` for failures
|
|
478
|
+
|
|
479
|
+
### ⚠️ Important: TracedQueries Run as Mutations
|
|
480
|
+
|
|
481
|
+
**Critical difference from other traced functions**: `tracedQuery` functions run
|
|
482
|
+
as **mutations**, not queries. This is necessary to enable tracing (which
|
|
483
|
+
requires writes to the trace tables), but it has important implications:
|
|
484
|
+
|
|
485
|
+
#### Breaking Change: Loss of Reactivity
|
|
486
|
+
|
|
487
|
+
Unlike regular Convex queries, `tracedQuery` results **do not update
|
|
488
|
+
reactively**. This means:
|
|
489
|
+
|
|
490
|
+
- ❌ Your UI won't automatically re-render when data changes
|
|
491
|
+
- ❌ You lose Convex's real-time subscription benefits
|
|
492
|
+
- ❌ You must manually refetch to get updated data
|
|
493
|
+
|
|
494
|
+
#### When to Use TracedQueries
|
|
495
|
+
|
|
496
|
+
Use `tracedQuery` only when you specifically need tracing:
|
|
497
|
+
|
|
498
|
+
- ✅ Understanding complex data flows
|
|
499
|
+
- ✅ Data fetches that don't need reactivity
|
|
500
|
+
- ✅ Debugging production issues
|
|
501
|
+
- ✅ Performance profiling
|
|
502
|
+
|
|
503
|
+
**For normal queries that need reactivity, use regular `query()` functions.**
|
|
504
|
+
|
|
505
|
+
#### Pattern: Shared Logic Between Queries and TracedQueries
|
|
506
|
+
|
|
507
|
+
To maintain both reactive queries for your UI and traced queries for debugging,
|
|
508
|
+
extract your business logic into a shared helper:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
// convex/helpers/products.ts
|
|
512
|
+
import { QueryCtx } from "../_generated/server";
|
|
513
|
+
import { Id } from "./_generated/dataModel";
|
|
514
|
+
|
|
515
|
+
// Shared business logic - no tracing
|
|
516
|
+
export async function getProductWithInventoryLogic(
|
|
517
|
+
ctx: Pick<QueryCtx, "db">,
|
|
518
|
+
productId: Id<"products">,
|
|
519
|
+
) {
|
|
520
|
+
const product = await ctx.db.get(productId);
|
|
521
|
+
if (!product) {
|
|
522
|
+
throw new Error("Product not found");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const inventory = await ctx.db
|
|
526
|
+
.query("inventory")
|
|
527
|
+
.withIndex("by_product", (q) => q.eq("productId", productId))
|
|
528
|
+
.first();
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
...product,
|
|
532
|
+
inventory: inventory?.quantity || 0,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
// convex/products.ts
|
|
539
|
+
import { v } from "convex/values";
|
|
540
|
+
import { query } from "./_generated/server";
|
|
541
|
+
import { tracedQuery } from "./tracer";
|
|
542
|
+
import { getProductWithInventoryLogic } from "./helpers/products";
|
|
543
|
+
|
|
544
|
+
// Regular query - USE THIS IN YOUR UI (reactive)
|
|
545
|
+
export const getProductWithInventory = query({
|
|
546
|
+
args: { productId: v.id("products") },
|
|
547
|
+
handler: async (ctx, { productId }) => {
|
|
548
|
+
return await getProductWithInventoryLogic(ctx, productId);
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Traced query (not reactive)
|
|
553
|
+
export const getProductWithInventoryTraced = tracedQuery({
|
|
554
|
+
name: "getProductWithInventory",
|
|
555
|
+
args: { productId: v.string() },
|
|
556
|
+
logArgs: ["productId"],
|
|
557
|
+
onSuccess: async (ctx, args, result) => {
|
|
558
|
+
if (result.inventory < 10) {
|
|
559
|
+
await ctx.tracer.warn("Low inventory", {
|
|
560
|
+
productId: args.productId,
|
|
561
|
+
inventory: result.inventory,
|
|
562
|
+
});
|
|
563
|
+
await ctx.tracer.preserve();
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
handler: async (ctx, { productId }) => {
|
|
567
|
+
await ctx.tracer.info("Fetching product", { productId });
|
|
568
|
+
|
|
569
|
+
// Reuse the same business logic
|
|
570
|
+
const result = await getProductWithInventoryLogic(ctx, productId);
|
|
571
|
+
|
|
572
|
+
await ctx.tracer.info("Product fetched", {
|
|
573
|
+
productId,
|
|
574
|
+
inventory: result.inventory,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
For queries that sometimes need tracing, you can conditionally call either
|
|
583
|
+
version
|
|
584
|
+
|
|
585
|
+
<!-- END: Include on https://convex.dev/components -->
|
|
586
|
+
|
|
587
|
+
Run the example:
|
|
588
|
+
|
|
589
|
+
```sh
|
|
590
|
+
npm i
|
|
591
|
+
npm run dev
|
|
592
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=_ignore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.d.ts","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.js","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":";AAAA,kEAAkE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GenericDataModel } from "convex/server";
|
|
2
|
+
import type { ObjectType, PropertyValidators } from "convex/values";
|
|
3
|
+
import type { ComponentApi } from "../component/_generated/component";
|
|
4
|
+
import type { ArgsWithTraceContext, LogArgs, OptionalArgsObject, StrippedGenericFunctionContext, TraceContext, TracedFunctionOptions, TracedResult, TracerConfig, TracerHandler } from "./types";
|
|
5
|
+
export declare function extractTraceContext<Args extends Record<string, unknown>>(allArgs: ArgsWithTraceContext<Args>): {
|
|
6
|
+
existingContext?: TraceContext;
|
|
7
|
+
args: Args;
|
|
8
|
+
};
|
|
9
|
+
export declare function prepareLogArgs<Args extends PropertyValidators>(args: ObjectType<Args>, logArgs: LogArgs<Args>): unknown | undefined;
|
|
10
|
+
export declare function setupTraceContext(ctx: StrippedGenericFunctionContext<GenericDataModel>, component: ComponentApi, existingContext: TraceContext | undefined, startTime: number, functionName: string, sampleRate: number, retentionMinutes: number, preserveErrors: boolean, spanData: {
|
|
11
|
+
functionName?: string;
|
|
12
|
+
args?: unknown;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
traceId: string;
|
|
15
|
+
spanId: string;
|
|
16
|
+
traceContext: TraceContext;
|
|
17
|
+
isRoot: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function executeTracedHandler<Args extends PropertyValidators, Output, EnhancedCtx>(params: {
|
|
20
|
+
ctx: StrippedGenericFunctionContext<GenericDataModel>;
|
|
21
|
+
component: ComponentApi;
|
|
22
|
+
traceId: string;
|
|
23
|
+
spanId: string;
|
|
24
|
+
startTime: number;
|
|
25
|
+
config: TracedFunctionOptions<EnhancedCtx, Args, Output> & TracerConfig;
|
|
26
|
+
args: OptionalArgsObject<Args>;
|
|
27
|
+
handler: TracerHandler<EnhancedCtx, Args>;
|
|
28
|
+
enhancedCtx: EnhancedCtx;
|
|
29
|
+
isRoot: boolean;
|
|
30
|
+
}): Promise<TracedResult<Output>>;
|
|
31
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/client/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,KAAK,EACV,oBAAoB,EACpB,OAAO,EACP,kBAAkB,EAClB,8BAA8B,EAC9B,YAAY,EACZ,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,aAAa,EACd,MAAM,SAAS,CAAC;AAajB,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtE,OAAO,EAAE,oBAAoB,CAAC,IAAI,CAAC,GAClC;IAAE,eAAe,CAAC,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAKhD;AAED,wBAAgB,cAAc,CAAC,IAAI,SAAS,kBAAkB,EAC5D,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,EACtB,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GACrB,OAAO,GAAG,SAAS,CAOrB;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,8BAA8B,CAAC,gBAAgB,CAAC,EACrD,SAAS,EAAE,YAAY,EACvB,eAAe,EAAE,YAAY,GAAG,SAAS,EACzC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,cAAc,EAAE,OAAO,EACvB,QAAQ,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAClD,OAAO,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC,CA4DD;AAED,wBAAsB,oBAAoB,CACxC,IAAI,SAAS,kBAAkB,EAC/B,MAAM,EACN,WAAW,EACX,MAAM,EAAE;IACR,GAAG,EAAE,8BAA8B,CAAC,gBAAgB,CAAC,CAAC;IACtD,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;IACxE,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,EAAE,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAC1C,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAyIhC"}
|