@@ -238,141 +238,171 @@ Where to put this in your app:
238238- In drag-and-drop onDrop callback
239239- Auto-instrumentation will capture fetch spans; the explicit span adds business context
240240
241- **Backend — Upload Validation and Async Processing**
241+ **Backend — Upload Validation and Queue Job**
242+
243+ <Alert>
244+
245+ This example demonstrates proper queue instrumentation patterns. For more details on instrumenting queues, see the <PlatformLink to="/tracing/instrumentation/queues-module/">Queues Module documentation</PlatformLink>.
246+
247+ </Alert>
242248
243249` ` ` typescript
244250// Import Sentry instrumentation first (required for v10)
245251import ' ./instrument' ;
246252import express from ' express' ;
247253import * as Sentry from ' @sentry/node' ;
248254
249- // POST /api/upload - Receive and validate upload, then trigger async processing
255+ // POST /api/upload - Receive and validate upload, then enqueue for processing
250256app .post (' /api/upload' , async (req : Request < {}, {}, UploadRequest > , res : Response ) => {
251257 const { fileName , fileType , fileSize } = req .body ;
252258
253- // Span 2: Backend validates and accepts upload
259+ // Validate the upload
260+ if (! fileName || ! fileType || ! fileSize) {
261+ return res .status (400 ).json ({ error: ' Missing required fields' });
262+ }
263+
264+ if (fileSize > 50 * 1024 * 1024 ) { // 50MB limit
265+ return res .status (400 ).json ({ error: ' File too large (max 50MB)' });
266+ }
267+
268+ // Create a job for processing
269+ const job = createJob (fileName, fileType, fileSize);
270+
271+ // Producer span: Enqueue media processing job
254272 await Sentry .startSpan (
255273 {
256- op: ' upload.receive ' ,
257- name: ' Receive upload ' ,
274+ op: ' queue.publish ' ,
275+ name: ' queue_producer ' ,
258276 attributes: {
259- ' file.name' : fileName,
260- ' file.size_bytes' : fileSize,
261- ' file.mime_type' : fileType,
262- ' validation.passed' : true
277+ ' messaging.message.id' : job .id ,
278+ ' messaging.destination.name' : ' media-processing' ,
279+ ' messaging.message.body.size' : fileSize,
263280 }
264281 },
265- async (span ) => {
266- try {
267- // Validate the upload
268- if (! fileName || ! fileType || ! fileSize) {
269- span? .setAttribute (' validation.passed' , false );
270- span? .setAttribute (' validation.error' , ' Missing required fields' );
271- return res .status (400 ).json ({ error: ' Missing required fields' });
272- }
273-
274- if (fileSize > 50 * 1024 * 1024 ) { // 50MB limit
275- span? .setAttribute (' validation.passed' , false );
276- span? .setAttribute (' validation.error' , ' File too large' );
277- return res .status (400 ).json ({ error: ' File too large (max 50MB)' });
278- }
279-
280- // Create a job for processing
281- const job = createJob (fileName, fileType, fileSize);
282- span? .setAttribute (' job.id' , job .id );
283-
284- // Start async processing (Span 3 will be created here)
285- setImmediate (async () => {
286- await processMedia (job);
287- });
288-
289- // Respond immediately with job ID
290- res .json ({
291- jobId: job .id ,
292- status: ' accepted' ,
293- message: ' Upload received and processing started'
294- });
295-
296- } catch (error) {
297- span? .setAttribute (' validation.passed' , false );
298- span? .setAttribute (' error.message' , error instanceof Error ? error .message : ' Unknown error' );
299- Sentry .captureException (error);
300- res .status (500 ).json ({ error: ' Failed to process upload' });
301- }
282+ async () => {
283+ // Get trace headers to pass to consumer
284+ const { 'sentry -trace ': sentryTrace , baggage: sentryBaggage } = Sentry .getTraceData ();
285+
286+ // Store job with trace headers for async processing
287+ await enqueueJob ({
288+ ... job,
289+ sentryTrace,
290+ sentryBaggage,
291+ enqueuedAt: Date .now (),
292+ });
293+
294+ // Start async processing
295+ setImmediate (async () => {
296+ await processMedia (job);
297+ });
298+
299+ // Respond immediately with job ID
300+ res .json ({
301+ jobId: job .id ,
302+ status: ' accepted' ,
303+ message: ' Upload received and processing started'
304+ });
302305 }
303306 );
304307});
305308` ` `
306309
307- **Backend — Async media processing **
310+ **Backend — Async Media Processing (Consumer) **
308311
309312` ` ` typescript
310313// Async media processing (runs in background via setImmediate)
311314export async function processMedia (job : ProcessingJob ): Promise<void> {
312- await Sentry .startSpan (
313- {
314- op: ' media.process' ,
315- name: ' Process media' ,
316- attributes: {
317- ' media.size_bytes' : job .fileSize ,
318- ' media.mime_type' : job .fileType ,
319- ' media.size_bucket' : getSizeBucket (job .fileSize ),
320- ' job.id' : job .id
321- }
322- },
323- async (span ) => {
324- try {
325- const startTime = Date .now ();
326- const operations: string [] = [];
327-
328- // Simulate image optimization and thumbnail generation
329- if (job .fileType .startsWith (' image/' )) {
330- // Note: No separate spans for these operations - use attributes instead
331- await optimizeImage (); // Simulated delay
332- operations .push (' optimize' );
333-
334- await generateThumbnail (); // Simulated delay
335- operations .push (' thumbnail' );
315+ // Continue trace from producer using stored trace headers
316+ Sentry .continueTrace (
317+ { sentryTrace: job .sentryTrace , baggage: job .sentryBaggage },
318+ () => {
319+ // Parent span for the consumer transaction
320+ Sentry .startSpan (
321+ {
322+ name: ' media_processing_consumer' ,
323+ },
324+ (parentSpan ) => {
325+ // Consumer span: Process the queued job
326+ Sentry .startSpan (
327+ {
328+ op: ' queue.process' ,
329+ name: ' queue_consumer' ,
330+ attributes: {
331+ ' messaging.message.id' : job .id ,
332+ ' messaging.destination.name' : ' media-processing' ,
333+ ' messaging.message.body.size' : job .fileSize ,
334+ ' messaging.message.receive.latency' : Date .now () - job .enqueuedAt ,
335+ ' messaging.message.retry.count' : 0 ,
336+ }
337+ },
338+ async (span ) => {
339+ try {
340+ const startTime = Date .now ();
341+ const operations: string [] = [];
342+
343+ // Add job-specific attributes
344+ span? .setAttribute (' media.size_bytes' , job .fileSize );
345+ span? .setAttribute (' media.mime_type' , job .fileType );
346+ span? .setAttribute (' media.size_bucket' , getSizeBucket (job .fileSize ));
347+
348+ // Simulate image optimization and thumbnail generation
349+ if (job .fileType .startsWith (' image/' )) {
350+ // Note: No separate spans for these operations - use attributes instead
351+ await optimizeImage (); // Simulated delay
352+ operations .push (' optimize' );
353+
354+ await generateThumbnail (); // Simulated delay
355+ operations .push (' thumbnail' );
356+ }
357+
358+ // Calculate results
359+ const sizeSaved = Math .floor (job .fileSize * 0.3 ); // 30% reduction
360+ const thumbnailCreated = Math .random () > 0.05 ; // 95% success rate
361+
362+ // Rich attributes instead of multiple spans
363+ span? .setAttribute (' processing.operations' , JSON .stringify (operations));
364+ span? .setAttribute (' processing.optimization_level' , ' high' );
365+ span? .setAttribute (' processing.thumbnail_created' , thumbnailCreated);
366+ span? .setAttribute (' processing.duration_ms' , Date .now () - startTime);
367+ span? .setAttribute (' result.size_saved_bytes' , sizeSaved);
368+ span? .setAttribute (' result.size_reduction_percent' , 30 );
369+ span? .setAttribute (' result.status' , ' success' );
370+
371+ // Update job status
372+ job .status = ' completed' ;
373+
374+ // Mark parent span as successful
375+ parentSpan .setStatus ({ code: 1 , message: ' ok' });
376+
377+ } catch (error) {
378+ span? .setAttribute (' result.status' , ' failed' );
379+ span? .setAttribute (' error.message' , error instanceof Error ? error .message : ' Unknown error' );
380+ parentSpan .setStatus ({ code: 2 , message: ' error' });
381+ Sentry .captureException (error);
382+ }
383+ }
384+ );
336385 }
337-
338- // Calculate results
339- const sizeSaved = Math .floor (job .fileSize * 0.3 ); // 30% reduction
340- const thumbnailCreated = Math .random () > 0.05 ; // 95% success rate
341-
342- // Rich attributes instead of multiple spans
343- span? .setAttribute (' processing.operations' , JSON .stringify (operations));
344- span? .setAttribute (' processing.optimization_level' , ' high' );
345- span? .setAttribute (' processing.thumbnail_created' , thumbnailCreated);
346- span? .setAttribute (' processing.duration_ms' , Date .now () - startTime);
347- span? .setAttribute (' result.size_saved_bytes' , sizeSaved);
348- span? .setAttribute (' result.size_reduction_percent' , 30 );
349- span? .setAttribute (' result.status' , ' success' );
350-
351- // Update job status
352- job .status = ' completed' ;
353-
354- } catch (error) {
355- span? .setAttribute (' result.status' , ' failed' );
356- span? .setAttribute (' error.message' , error instanceof Error ? error .message : ' Unknown error' );
357- Sentry .captureException (error);
358- }
386+ );
359387 }
360388 );
361389}
362390` ` `
363391
364392**How the trace works together:**
365393- Frontend span (` file .upload ` ) captures the entire user experience from file selection to server response.
366- - Backend validation span (` upload .receive ` ) tracks server-side validation and job creation.
367- - Async processing span (` media .process ` ) runs in background with rich attributes for all processing operations.
368- - No unnecessary spans for individual operations — prefer attributes for details.
369- - Trace continuity is maintained via Sentry’s automatic context propagation.
394+ - Backend producer span (` queue .publish ` ) tracks job enqueueing with proper queue attributes.
395+ - Consumer span (` queue .process ` ) continues the trace using ` continueTrace ()` with trace headers stored in the job.
396+ - Async processing runs independently with its own trace connected via queue instrumentation.
397+ - Rich attributes on the consumer span capture all processing details without creating excessive child spans.
398+ - This pattern populates Sentry's Queues insights page for monitoring queue performance.
370399
371400What to monitor with span metrics:
372401- p95 upload duration by ` file .size_bucket ` .
373402- Processing success rate by ` media .mime_type ` .
374403- Average storage saved via ` result .size_saved_bytes ` where ` result .status = success` .
375- - Validation failure reasons grouped by ` validation .error ` .
404+ - Queue latency via ` messaging .message .receive .latency ` to track processing delays.
405+ - Job throughput via ` op: queue .publish ` and ` op: queue .process ` span counts.
376406
377407## Search Autocomplete (debounced, cancellable, performance monitoring)
378408
@@ -387,15 +417,16 @@ Example Repository: [NullFlix](https://github.com/getsentry/nullflix-tracing-exa
387417` ` ` typescript
388418const response = await Sentry .startSpan (
389419 {
390- op: ' http.client ' ,
391- name: ' Search autocomplete' ,
420+ op: ' function ' ,
421+ name: ' Search autocomplete request ' ,
392422 attributes: {
393423 ' query.length' : searchQuery .length ,
394424 ' ui.debounce_ms' : DEBOUNCE_MS ,
395425 },
396426 },
397427 async (span ) => {
398428 try {
429+ // SDK automatically instruments the fetch with op: 'http.client'
399430 const response = await fetch (
400431 ` ${ API_URL } /api/search?${ new URLSearchParams ({ q: searchQuery })} ` ,
401432 {
@@ -422,7 +453,8 @@ const response = await Sentry.startSpan(
422453 if (error instanceof Error && error .name === ' AbortError' ) {
423454 span? .setAttribute (' ui.aborted' , true );
424455 span? .setStatus ({ code: 2 , message: ' cancelled' });
425- throw error;
456+ // Don't re-throw AbortError to avoid sending it to Sentry
457+ return { results: [] };
426458 }
427459
428460 span? .setStatus ({ code: 2 , message: error instanceof Error ? error .message : ' unknown error' });
@@ -518,6 +550,12 @@ Example Repository: _Coming soon - sample repository in development_
518550
519551**Solution:** Manually instrument each component of the AI pipeline using Sentry's AI agent span conventions. Create spans for agent invocation, LLM calls, tool executions, and handoffs between agents, with rich attributes for monitoring costs, performance, and business metrics.
520552
553+ <Alert>
554+
555+ This example follows Sentry's AI Agents Module conventions. For detailed specifications on ` gen_ai.* ` span attributes and requirements, see the [AI Agents Module documentation](https://develop.sentry.dev/sdk/telemetry/traces/modules/ai-agents/).
556+
557+ </Alert>
558+
521559
522560
523561**Frontend (React) — Instrument AI Chat Interface:**
0 commit comments