Skip to content

Commit 8c82398

Browse files
Add request correlation ID tracking for improved error monitoring
Co-authored-by: me <[email protected]>
1 parent 7985a7c commit 8c82398

File tree

7 files changed

+156
-5
lines changed

7 files changed

+156
-5
lines changed

packages/workshop-app/app/components/error-boundary.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException } from '@sentry/react-router'
1+
import { captureException, withScope } from '@sentry/react-router'
22
import { useEffect } from 'react'
33
import {
44
isRouteErrorResponse,
@@ -7,6 +7,7 @@ import {
77
type ErrorResponse,
88
} from 'react-router'
99
import { getErrorMessage } from '#app/utils/misc.tsx'
10+
import { getCorrelationId } from '#app/utils/request-correlation'
1011

1112
type StatusHandler = (info: {
1213
error: ErrorResponse
@@ -33,7 +34,21 @@ export function GeneralErrorBoundary({
3334
useEffect(() => {
3435
if (isResponse) return
3536
if (ENV.EPICSHOP_IS_PUBLISHED) {
36-
captureException(error)
37+
const correlationId = getCorrelationId()
38+
if (correlationId) {
39+
withScope((scope) => {
40+
scope.setTag('correlationId', correlationId)
41+
scope.setExtra('correlationId', correlationId)
42+
scope.setContext('correlation', {
43+
id: correlationId,
44+
timestamp: new Date().toISOString(),
45+
component: 'ErrorBoundary',
46+
})
47+
captureException(error)
48+
})
49+
} else {
50+
captureException(error)
51+
}
3752
}
3853
}, [error, isResponse])
3954

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { hydrateRoot } from 'react-dom/client'
22
import { HydratedRouter } from 'react-router/dom'
3-
import { init as initMonitoring } from './utils/monitoring.client'
3+
import { init as initMonitoring, setupRequestCorrelation } from './utils/monitoring.client'
44

55
initMonitoring()
6+
setupRequestCorrelation()
67

78
hydrateRoot(document, <HydratedRouter />)

packages/workshop-app/app/entry.server.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,23 @@ export function handleError(
1919
): void {
2020
if (request.signal.aborted) return
2121
if (ENV.EPICSHOP_IS_PUBLISHED) {
22-
Sentry.captureException(error)
22+
// Add correlation ID to error context
23+
const correlationId = request.headers.get('x-correlation-id')
24+
if (correlationId) {
25+
Sentry.withScope((scope) => {
26+
scope.setTag('correlationId', correlationId)
27+
scope.setExtra('correlationId', correlationId)
28+
scope.setContext('correlation', {
29+
id: correlationId,
30+
timestamp: new Date().toISOString(),
31+
url: request.url,
32+
method: request.method,
33+
})
34+
Sentry.captureException(error)
35+
})
36+
} else {
37+
Sentry.captureException(error)
38+
}
2339
}
2440
}
2541

packages/workshop-app/app/utils/monitoring.client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Sentry from '@sentry/react-router'
2+
import { generateCorrelationId, setCorrelationId, getCorrelationId, CORRELATION_ID_HEADER } from './request-correlation'
23

34
export function init() {
45
if (!ENV.EPICSHOP_IS_PUBLISHED) return
@@ -16,6 +17,20 @@ export function init() {
1617
return null
1718
}
1819
}
20+
21+
// Add correlation ID to event context
22+
const correlationId = getCorrelationId()
23+
if (correlationId) {
24+
event.extra = {
25+
...event.extra,
26+
correlationId,
27+
}
28+
event.tags = {
29+
...event.tags,
30+
correlationId,
31+
}
32+
}
33+
1934
return event
2035
},
2136
integrations: [
@@ -27,3 +42,25 @@ export function init() {
2742
replaysOnErrorSampleRate: 1.0,
2843
})
2944
}
45+
46+
// Set up request correlation for React Router requests
47+
export function setupRequestCorrelation() {
48+
if (!ENV.EPICSHOP_IS_PUBLISHED) return
49+
50+
// Intercept React Router requests by patching fetch
51+
const originalFetch = window.fetch
52+
window.fetch = function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
53+
const correlationId = generateCorrelationId()
54+
setCorrelationId(correlationId)
55+
56+
const headers = new Headers(init?.headers)
57+
headers.set(CORRELATION_ID_HEADER, correlationId)
58+
59+
const newInit = {
60+
...init,
61+
headers,
62+
}
63+
64+
return originalFetch(input, newInit)
65+
}
66+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createId } from '@paralleldrive/cuid2'
2+
3+
// Header name for request correlation
4+
export const CORRELATION_ID_HEADER = 'x-correlation-id'
5+
6+
// Generate a unique correlation ID
7+
export function generateCorrelationId(): string {
8+
return createId()
9+
}
10+
11+
// Store correlation ID in context (client-side)
12+
let currentCorrelationId: string | null = null
13+
14+
export function setCorrelationId(id: string): void {
15+
currentCorrelationId = id
16+
}
17+
18+
export function getCorrelationId(): string | null {
19+
return currentCorrelationId
20+
}
21+
22+
export function clearCorrelationId(): void {
23+
currentCorrelationId = null
24+
}
25+
26+
// Context for storing correlation ID during request processing (server-side)
27+
export const requestCorrelationContext = new Map<string, string>()
28+
29+
export function setRequestCorrelationId(requestId: string, correlationId: string): void {
30+
requestCorrelationContext.set(requestId, correlationId)
31+
}
32+
33+
export function getRequestCorrelationId(requestId: string): string | null {
34+
return requestCorrelationContext.get(requestId) || null
35+
}
36+
37+
export function clearRequestCorrelationId(requestId: string): void {
38+
requestCorrelationContext.delete(requestId)
39+
}

packages/workshop-app/instrument.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ Sentry.init({
1313
/\/favicon.ico/,
1414
/\/site\.webmanifest/,
1515
],
16-
integrations: [Sentry.httpIntegration(), nodeProfilingIntegration()],
16+
integrations: [
17+
Sentry.httpIntegration(),
18+
nodeProfilingIntegration(),
19+
{
20+
name: 'CorrelationIdIntegration',
21+
setupOnce() {
22+
// This will be used to add correlation IDs to all events
23+
},
24+
},
25+
],
1726
tracesSampler(samplingContext) {
1827
if (samplingContext.request?.url?.includes('/resources/healthcheck')) {
1928
return 0
@@ -26,4 +35,19 @@ Sentry.init({
2635
}
2736
return event
2837
},
38+
beforeSend(event) {
39+
// Add correlation ID to server-side events
40+
const correlationId = Sentry.getActiveSpan()?.getAttribute('correlationId')
41+
if (correlationId) {
42+
event.extra = {
43+
...event.extra,
44+
correlationId,
45+
}
46+
event.tags = {
47+
...event.tags,
48+
correlationId,
49+
}
50+
}
51+
return event
52+
},
2953
})

packages/workshop-app/server/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ if (!ENV.EPICSHOP_DEPLOYED) {
195195
})
196196
}
197197

198+
// Add correlation ID middleware before React Router handler
199+
app.use((req, res, next) => {
200+
const correlationId = req.headers['x-correlation-id'] as string
201+
if (correlationId && ENV.EPICSHOP_IS_PUBLISHED) {
202+
// Add correlation ID to current Sentry scope
203+
import('@sentry/react-router').then(({ withScope }) => {
204+
withScope((scope) => {
205+
scope.setTag('correlationId', correlationId)
206+
scope.setExtra('correlationId', correlationId)
207+
scope.setContext('correlation', {
208+
id: correlationId,
209+
timestamp: new Date().toISOString(),
210+
})
211+
})
212+
})
213+
}
214+
next()
215+
})
216+
198217
app.all(
199218
'*splat',
200219
createRequestHandler({

0 commit comments

Comments
 (0)