|
1 | | -import type { RequestEventData, WebFetchHeaders } from '@sentry/core'; |
| 1 | +import type { RequestEventData } from '@sentry/core'; |
2 | 2 | import { |
3 | 3 | captureException, |
4 | 4 | getActiveSpan, |
5 | | - getCapturedScopesOnSpan, |
6 | | - getRootSpan, |
| 5 | + getIsolationScope, |
7 | 6 | handleCallbackErrors, |
8 | | - propagationContextFromHeaders, |
9 | | - Scope, |
10 | | - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
11 | | - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, |
12 | | - setCapturedScopesOnSpan, |
13 | 7 | SPAN_STATUS_ERROR, |
14 | 8 | SPAN_STATUS_OK, |
15 | | - startSpanManual, |
16 | 9 | winterCGHeadersToDict, |
17 | | - withIsolationScope, |
18 | | - withScope, |
19 | 10 | } from '@sentry/core'; |
20 | 11 | import type { GenerationFunctionContext } from '../common/types'; |
21 | 12 | import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; |
22 | | -import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; |
23 | | -import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; |
| 13 | +import { flushSafelyWithTimeout, waitUntil } from './utils/responseEnd'; |
| 14 | + |
24 | 15 | /** |
25 | | - * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. |
| 16 | + * Wraps a generation function (e.g. generateMetadata) with Sentry error instrumentation. |
26 | 17 | */ |
27 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
28 | 19 | export function wrapGenerationFunctionWithSentry<F extends (...args: any[]) => any>( |
29 | 20 | generationFunction: F, |
30 | 21 | context: GenerationFunctionContext, |
31 | 22 | ): F { |
32 | | - const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context; |
33 | 23 | return new Proxy(generationFunction, { |
34 | 24 | apply: (originalFunction, thisArg, args) => { |
35 | | - const requestTraceId = getActiveSpan()?.spanContext().traceId; |
36 | | - let headers: WebFetchHeaders | undefined = undefined; |
37 | | - // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API |
| 25 | + const isolationScope = getIsolationScope(); |
| 26 | + |
| 27 | + let headers = undefined; |
| 28 | + // We try-catch here just in case anything goes wrong with the async storage since it is Next.js internal API |
38 | 29 | try { |
39 | | - headers = requestAsyncStorage?.getStore()?.headers; |
| 30 | + headers = context.requestAsyncStorage?.getStore()?.headers; |
40 | 31 | } catch { |
41 | 32 | /** empty */ |
42 | 33 | } |
43 | 34 |
|
44 | | - const isolationScope = commonObjectToIsolationScope(headers); |
45 | | - |
46 | | - const activeSpan = getActiveSpan(); |
47 | | - if (activeSpan) { |
48 | | - const rootSpan = getRootSpan(activeSpan); |
49 | | - const { scope } = getCapturedScopesOnSpan(rootSpan); |
50 | | - setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); |
51 | | - } |
52 | | - |
53 | 35 | const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; |
54 | 36 |
|
55 | | - return withIsolationScope(isolationScope, () => { |
56 | | - return withScope(scope => { |
57 | | - scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); |
| 37 | + isolationScope.setSDKProcessingMetadata({ |
| 38 | + normalizedRequest: { |
| 39 | + headers: headersDict, |
| 40 | + } satisfies RequestEventData, |
| 41 | + }); |
58 | 42 |
|
59 | | - isolationScope.setSDKProcessingMetadata({ |
60 | | - normalizedRequest: { |
61 | | - headers: headersDict, |
62 | | - } satisfies RequestEventData, |
63 | | - }); |
| 43 | + return handleCallbackErrors( |
| 44 | + () => originalFunction.apply(thisArg, args), |
| 45 | + error => { |
| 46 | + const span = getActiveSpan(); |
| 47 | + const { componentRoute, componentType, generationFunctionIdentifier } = context; |
| 48 | + let shouldCapture = true; |
| 49 | + isolationScope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); |
64 | 50 |
|
65 | | - const activeSpan = getActiveSpan(); |
66 | | - if (activeSpan) { |
67 | | - const rootSpan = getRootSpan(activeSpan); |
68 | | - const sentryTrace = headersDict?.['sentry-trace']; |
69 | | - if (sentryTrace) { |
70 | | - rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); |
| 51 | + if (span) { |
| 52 | + if (isNotFoundNavigationError(error)) { |
| 53 | + // We don't want to report "not-found"s |
| 54 | + shouldCapture = false; |
| 55 | + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); |
| 56 | + } else if (isRedirectNavigationError(error)) { |
| 57 | + // We don't want to report redirects |
| 58 | + shouldCapture = false; |
| 59 | + span.setStatus({ code: SPAN_STATUS_OK }); |
| 60 | + } else { |
| 61 | + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); |
71 | 62 | } |
72 | 63 | } |
73 | 64 |
|
74 | | - const propagationContext = commonObjectToPropagationContext( |
75 | | - headers, |
76 | | - propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']), |
77 | | - ); |
78 | | - |
79 | | - if (requestTraceId) { |
80 | | - propagationContext.traceId = requestTraceId; |
81 | | - } |
82 | | - |
83 | | - scope.setPropagationContext(propagationContext); |
84 | | - |
85 | | - return startSpanManual( |
86 | | - { |
87 | | - op: 'function.nextjs', |
88 | | - name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, |
89 | | - attributes: { |
90 | | - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', |
91 | | - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', |
92 | | - 'sentry.nextjs.ssr.function.type': generationFunctionIdentifier, |
93 | | - 'sentry.nextjs.ssr.function.route': componentRoute, |
94 | | - }, |
95 | | - }, |
96 | | - span => { |
97 | | - return handleCallbackErrors( |
98 | | - () => originalFunction.apply(thisArg, args), |
99 | | - err => { |
100 | | - // When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?" |
101 | | - // The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response. |
102 | | - if (isNotFoundNavigationError(err)) { |
103 | | - // We don't want to report "not-found"s |
104 | | - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); |
105 | | - getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); |
106 | | - } else if (isRedirectNavigationError(err)) { |
107 | | - // We don't want to report redirects |
108 | | - span.setStatus({ code: SPAN_STATUS_OK }); |
109 | | - } else { |
110 | | - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); |
111 | | - getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); |
112 | | - captureException(err, { |
113 | | - mechanism: { |
114 | | - handled: false, |
115 | | - type: 'auto.function.nextjs.generation_function', |
116 | | - data: { |
117 | | - function: generationFunctionIdentifier, |
118 | | - }, |
119 | | - }, |
120 | | - }); |
121 | | - } |
122 | | - }, |
123 | | - () => { |
124 | | - span.end(); |
| 65 | + if (shouldCapture) { |
| 66 | + captureException(error, { |
| 67 | + mechanism: { |
| 68 | + handled: false, |
| 69 | + type: 'auto.function.nextjs.generation_function', |
| 70 | + data: { |
| 71 | + function: generationFunctionIdentifier, |
125 | 72 | }, |
126 | | - ); |
127 | | - }, |
128 | | - ); |
129 | | - }); |
130 | | - }); |
| 73 | + }, |
| 74 | + }); |
| 75 | + } |
| 76 | + }, |
| 77 | + () => { |
| 78 | + waitUntil(flushSafelyWithTimeout()); |
| 79 | + }, |
| 80 | + ); |
131 | 81 | }, |
132 | 82 | }); |
133 | 83 | } |
0 commit comments