-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathwrapServerComponentWithSentry.ts
128 lines (118 loc) · 4.95 KB
/
wrapServerComponentWithSentry.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import type { RequestEventData } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SPAN_STATUS_ERROR,
SPAN_STATUS_OK,
Scope,
captureException,
getActiveSpan,
getCapturedScopesOnSpan,
getRootSpan,
handleCallbackErrors,
propagationContextFromHeaders,
setCapturedScopesOnSpan,
startSpanManual,
vercelWaitUntil,
winterCGHeadersToDict,
withIsolationScope,
withScope,
} from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
import type { ServerComponentContext } from '../common/types';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
import { flushSafelyWithTimeout } from './utils/responseEnd';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
/**
* Wraps an `app` directory server component with Sentry error instrumentation.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>(
appDirComponent: F,
context: ServerComponentContext,
): F {
const { componentRoute, componentType } = context;
// Even though users may define server components as async functions, for the client bundles
// Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use`
// hook. 🤯
return new Proxy(appDirComponent, {
apply: (originalFunction, thisArg, args) => {
const requestTraceId = getActiveSpan()?.spanContext().traceId;
const isolationScope = commonObjectToIsolationScope(context.headers);
const activeSpan = getActiveSpan();
if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
const { scope } = getCapturedScopesOnSpan(rootSpan);
setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope);
}
const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;
isolationScope.setSDKProcessingMetadata({
normalizedRequest: {
headers: headersDict,
} satisfies RequestEventData,
});
return withIsolationScope(isolationScope, () => {
return withScope(scope => {
scope.setTransactionName(`${componentType} Server Component (${componentRoute})`);
if (process.env.NEXT_RUNTIME === 'edge') {
const propagationContext = commonObjectToPropagationContext(
context.headers,
propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']),
);
if (requestTraceId) {
propagationContext.traceId = requestTraceId;
}
scope.setPropagationContext(propagationContext);
}
const activeSpan = getActiveSpan();
if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
const sentryTrace = headersDict?.['sentry-trace'];
if (sentryTrace) {
rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace);
}
}
return startSpanManual(
{
op: 'function.nextjs',
name: `${componentType} Server Component (${componentRoute})`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
'sentry.nextjs.function.type': componentType,
'sentry.nextjs.function.route': componentRoute,
},
},
span => {
return handleCallbackErrors(
() => originalFunction.apply(thisArg, args),
error => {
// When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?"
// The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response.
if (isNotFoundNavigationError(error)) {
// We don't want to report "not-found"s
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
} else if (isRedirectNavigationError(error)) {
// We don't want to report redirects
span.setStatus({ code: SPAN_STATUS_OK });
} else {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
captureException(error, {
mechanism: {
handled: false,
},
});
}
},
() => {
span.end();
vercelWaitUntil(flushSafelyWithTimeout());
},
);
},
);
});
});
},
});
}