@@ -65,41 +65,28 @@ Where to put this in your app:
65
65
- In the ` onClick` for the checkout CTA, or inside the submit handler of your checkout form/container component.
66
66
- Auto-instrumentation will add client ` fetch` spans; keep the explicit UI span for business context.
67
67
68
- **Backend — instrument each business step :**
68
+ **Backend — single span per request; rely on auto-instrumentation :**
69
69
70
70
` ` ` javascript
71
71
// Example: Node/Express
72
72
app .post (" /api/checkout" , async (req , res ) => {
73
- await Sentry .startSpan ({ name: " Process order " , op: " business.logic " }, async (orderSpan ) => {
73
+ await Sentry .startSpan ({ name: " Checkout (server) " , op: " http.server " }, async (span ) => {
74
74
try {
75
- await Sentry .startSpan ({ name: " Validate cart" , op: " validate" }, async () => {
76
- // validate item availability, pricing, coupon
77
- });
78
-
79
- const payment = await Sentry .startSpan ({ name: " Authorize payment" , op: " payment" }, async (span ) => {
80
- span .setAttribute (" payment.provider" , " stripe" );
81
- // call payment provider
82
- return { id: " pi_123" , status: " authorized" };
83
- });
84
- orderSpan .setAttribute (" payment.status" , payment .status );
85
-
86
- await Sentry .startSpan ({ name: " Reserve inventory" , op: " inventory" }, async () => {
87
- // reserve stock in DB or external OMS
88
- });
89
-
90
- const orderId = await Sentry .startSpan ({ name: " Create order" , op: " db.write" }, async () => {
91
- // insert order; return internal id
92
- return " ord_abc123" ;
93
- });
94
- orderSpan .setAttribute (" order.id" , orderId);
95
-
96
- await Sentry .startSpan ({ name: " Send confirmation" , op: " email" }, async () => {
97
- // enqueue email or call provider
75
+ // validate, authorize payment, reserve inventory, create order, send email
76
+ const orderId = await createOrder (req .body );
77
+
78
+ // Keep attributes low-cardinality and business-focused
79
+ span .setAttributes ({
80
+ " order.id" : orderId,
81
+ " payment.provider" : " stripe" ,
82
+ " payment.status" : " authorized" ,
83
+ " inventory.reserved" : true ,
84
+ " email.enqueued" : true ,
98
85
});
99
86
100
87
res .json ({ orderId, paymentProvider: " stripe" });
101
88
} catch (e) {
102
- orderSpan .setStatus ? .(" error" );
89
+ span .setStatus ? .(" error" );
103
90
res .status (500 ).json ({ error: " Checkout failed" });
104
91
}
105
92
});
@@ -108,7 +95,7 @@ app.post("/api/checkout", async (req, res) => {
108
95
109
96
**How the trace works together:**
110
97
- UI span starts on click → fetch carries trace headers → backend continues the trace.
111
- - Child spans highlight where time is spent (validation, payment, inventory, DB, email) .
98
+ - Rely on your SDK's auto-instrumentation for HTTP/DB/external calls; keep this example span simple .
112
99
- Span metrics let you track latency percentiles and failure rates by attributes like ` payment .provider ` , ` cart .item_count ` .
113
100
114
101
What to monitor with span metrics:
@@ -122,7 +109,7 @@ What to monitor with span metrics:
122
109
123
110
**Solution:** Start a client span when the upload begins; on the backend, create spans for signed-URL issuance, enqueue a job, and instrument worker phases. Propagate trace context via job metadata to stitch the traces.
124
111
125
- **Frontend (React) — instrument upload begin and progress :**
112
+ **Frontend (React) — instrument upload begin and completion :**
126
113
127
114
` ` ` javascript
128
115
Sentry .startSpan (
@@ -132,27 +119,28 @@ Sentry.startSpan(
132
119
attributes: {
133
120
" file.size_bytes" : file .size ,
134
121
" file.mime_type" : file .type ,
135
- " upload.chunked" : true ,
136
122
},
137
123
},
138
124
async (span ) => {
125
+ const t0 = performance .now ();
139
126
try {
140
127
const urlRes = await fetch (" /api/uploads/signed-url" , { method: " POST" });
141
128
const { uploadUrl , objectKey } = await urlRes .json ();
142
129
143
- // Use XHR for progress; fetch is illustrative here
144
- await uploadWithProgress (uploadUrl, file, (progress ) => {
145
- span .setAttribute (" upload.bytes_transferred" , progress .bytes );
146
- span .setAttribute (" upload.percent_complete" , progress .percent );
147
- });
130
+ // Upload file to storage (signed URL)
131
+ await fetch (uploadUrl, { method: " PUT" , body: file });
148
132
133
+ // Tell backend to start async processing
149
134
await fetch (" /api/uploads/start-processing" , {
150
135
method: " POST" ,
151
136
headers: { " content-type" : " application/json" },
152
137
body: JSON .stringify ({ key: objectKey }),
153
138
});
154
139
155
- span .setAttribute (" upload.success" , true );
140
+ span .setAttributes ({
141
+ " upload.success" : true ,
142
+ " upload.duration_ms" : Math .round (performance .now () - t0),
143
+ });
156
144
} catch (e) {
157
145
span .setStatus ? .(" error" );
158
146
Sentry .captureException (e);
@@ -165,15 +153,13 @@ Sentry.startSpan(
165
153
166
154
Place this where the user triggers an upload (dropzone onDrop, file input onChange, or explicit Upload button).
167
155
168
- **Backend — signed URL, enqueue job, and worker phases :**
156
+ **Backend — single span per handler; single span in worker :**
169
157
170
158
` ` ` javascript
171
159
// Issue signed URL
172
160
app .post (" /api/uploads/signed-url" , async (req , res ) => {
173
- await Sentry .startSpan ({ name: " Get signed URL" , op: " storage.sign" }, async (span ) => {
174
- span .setAttribute (" storage.provider" , " s3" );
175
- span .setAttribute (" storage.bucket" , " media" );
176
- // generate URL
161
+ await Sentry .startSpan ({ name: " Signed URL" , op: " storage.sign" }, async (span ) => {
162
+ span .setAttributes ({ " storage.provider" : " s3" , " storage.bucket" : " media" });
177
163
res .json ({ uploadUrl: " https://s3..." , objectKey: " uploads/abc.jpg" });
178
164
});
179
165
});
@@ -191,14 +177,13 @@ app.post("/api/uploads/start-processing", async (req, res) => {
191
177
// Worker
192
178
worker .on (" message" , async (msg ) => {
193
179
const parentContext = msg .trace ; // restore trace/parent if available
194
- await Sentry .startSpan ({ name: " Process media" , op: " worker.job" , parentContext }, async (jobSpan ) => {
195
- await Sentry .startSpan ({ name: " Virus scan" , op: " security.scan" }, async (span ) => {
196
- span .setAttribute (" scan.engine" , " clamav" );
180
+ await Sentry .startSpan ({ name: " Process media" , op: " worker.job" , parentContext }, async (span ) => {
181
+ // Do work (scan, transcode, thumbnail) — rely on auto-instrumentation for sub-operations
182
+ span .setAttributes ({
183
+ " scan.engine" : " clamav" ,
184
+ " transcode.preset" : " 720p" ,
185
+ " thumbnail.created" : true ,
197
186
});
198
- await Sentry .startSpan ({ name: " Transcode" , op: " media.transcode" }, async (span ) => {
199
- span .setAttribute (" transcode.preset" , " 720p" );
200
- });
201
- await Sentry .startSpan ({ name: " Thumbnail" , op: " media.thumbnail" }, async () => {});
202
187
});
203
188
});
204
189
` ` `
@@ -269,31 +254,21 @@ async function runSearch(query, debounceMs = 150) {
269
254
270
255
Place this in your search input hook/component after applying a debounce.
271
256
272
- **Backend — cache, search engine, rank :**
257
+ **Backend — single span with useful attributes :**
273
258
274
259
` ` ` javascript
275
260
app .get (" /api/search" , async (req , res ) => {
276
261
const q = String (req .query .q || " " );
277
-
278
- await Sentry .startSpan ({ name: " Search" , op: " search" }, async (root ) => {
279
- const cacheHit = await Sentry .startSpan ({ name: " Cache lookup" , op: " cache" }, async (span ) => {
280
- const hit = await cache .get (q);
281
- span .setAttribute (" cache.hit" , Boolean (hit));
282
- if (hit) res .json (hit);
283
- return Boolean (hit);
284
- });
285
- if (cacheHit) return ;
286
-
287
- const results = await Sentry .startSpan ({ name: " Query engine" , op: " external.search" }, async (span ) => {
288
- span .setAttribute (" search.engine" , " elasticsearch" );
289
- span .setAttribute (" search.index" , " products_v2" );
290
- span .setAttribute (" search.mode" , q .length < 3 ? " prefix" : " fuzzy" );
291
- return searchEngine .query (q);
292
- });
293
-
294
- await Sentry .startSpan ({ name: " Rank results" , op: " compute.rank" }, async (span ) => {
295
- span .setAttribute (" rank.model" , " bm25" );
296
- span .setAttribute (" rank.version" , " 2.1" );
262
+ await Sentry .startSpan ({ name: " Search" , op: " search" }, async (span ) => {
263
+ const hit = await cache .get (q);
264
+ span .setAttribute (" cache.hit" , Boolean (hit));
265
+ if (hit) return res .json (hit);
266
+
267
+ const results = await searchEngine .query (q);
268
+ span .setAttributes ({
269
+ " search.engine" : " elasticsearch" ,
270
+ " search.mode" : q .length < 3 ? " prefix" : " fuzzy" ,
271
+ " results.count" : results .length ,
297
272
});
298
273
299
274
res .json ({ results });
@@ -303,7 +278,7 @@ app.get("/api/search", async (req, res) => {
303
278
304
279
**How the trace works together:**
305
280
- Each debounced request becomes a client span; aborted ones are marked and short.
306
- - Server spans show cache effectiveness, engine latency, and ranking time .
281
+ - The server span shows cache effectiveness and search mode; auto-instrumentation will cover network/DB latency .
307
282
308
283
What to monitor with span metrics:
309
284
- p95 latency of ` op: http .client ` by ` query .length ` bucket.
0 commit comments