|
1 |
| -import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node'; |
| 1 | +import { |
| 2 | + captureException, |
| 3 | + configureScope, |
| 4 | + continueTrace, |
| 5 | + getCurrentHub, |
| 6 | + runWithAsyncContext, |
| 7 | + startSpan, |
| 8 | +} from '@sentry/node'; |
2 | 9 | import type { Hub, Span } from '@sentry/types';
|
3 | 10 | import {
|
4 | 11 | addNonEnumerableProperty,
|
@@ -56,107 +63,125 @@ type AstroLocalsWithSentry = Record<string, unknown> & {
|
56 | 63 | __sentry_wrapped__?: boolean;
|
57 | 64 | };
|
58 | 65 |
|
59 |
| -export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseHandler = ( |
60 |
| - options = { trackClientIp: false, trackHeaders: false }, |
61 |
| -) => { |
| 66 | +export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseHandler = options => { |
| 67 | + const handlerOptions = { trackClientIp: false, trackHeaders: false, ...options }; |
| 68 | + |
62 | 69 | return async (ctx, next) => {
|
63 |
| - // Make sure we don't accidentally double wrap (e.g. user added middleware and integration auto added it) |
64 |
| - const locals = ctx.locals as AstroLocalsWithSentry; |
65 |
| - if (locals && locals.__sentry_wrapped__) { |
66 |
| - return next(); |
| 70 | + // if there is an active span, we know that this handle call is nested and hence |
| 71 | + // we don't create a new domain for it. If we created one, nested server calls would |
| 72 | + // create new transactions instead of adding a child span to the currently active span. |
| 73 | + if (getCurrentHub().getScope().getSpan()) { |
| 74 | + return instrumentRequest(ctx, next, handlerOptions); |
67 | 75 | }
|
68 |
| - addNonEnumerableProperty(locals, '__sentry_wrapped__', true); |
| 76 | + return runWithAsyncContext(() => { |
| 77 | + return instrumentRequest(ctx, next, handlerOptions); |
| 78 | + }); |
| 79 | + }; |
| 80 | +}; |
69 | 81 |
|
70 |
| - const method = ctx.request.method; |
71 |
| - const headers = ctx.request.headers; |
| 82 | +async function instrumentRequest( |
| 83 | + ctx: Parameters<MiddlewareResponseHandler>[0], |
| 84 | + next: Parameters<MiddlewareResponseHandler>[1], |
| 85 | + options: MiddlewareOptions, |
| 86 | +): Promise<Response> { |
| 87 | + // Make sure we don't accidentally double wrap (e.g. user added middleware and integration auto added it) |
| 88 | + const locals = ctx.locals as AstroLocalsWithSentry; |
| 89 | + if (locals && locals.__sentry_wrapped__) { |
| 90 | + return next(); |
| 91 | + } |
| 92 | + addNonEnumerableProperty(locals, '__sentry_wrapped__', true); |
72 | 93 |
|
73 |
| - const { dynamicSamplingContext, traceparentData, propagationContext } = tracingContextFromHeaders( |
74 |
| - headers.get('sentry-trace') || undefined, |
75 |
| - headers.get('baggage'), |
76 |
| - ); |
| 94 | + const { method, headers } = ctx.request; |
77 | 95 |
|
78 |
| - const allHeaders: Record<string, string> = {}; |
| 96 | + const traceCtx = continueTrace({ |
| 97 | + sentryTrace: headers.get('sentry-trace') || undefined, |
| 98 | + baggage: headers.get('baggage'), |
| 99 | + }); |
| 100 | + |
| 101 | + const allHeaders: Record<string, string> = {}; |
| 102 | + |
| 103 | + if (options.trackHeaders) { |
79 | 104 | headers.forEach((value, key) => {
|
80 | 105 | allHeaders[key] = value;
|
81 | 106 | });
|
| 107 | + } |
82 | 108 |
|
| 109 | + if (options.trackClientIp) { |
83 | 110 | configureScope(scope => {
|
84 |
| - scope.setPropagationContext(propagationContext); |
85 |
| - |
86 |
| - if (options.trackClientIp) { |
87 |
| - scope.setUser({ ip_address: ctx.clientAddress }); |
88 |
| - } |
| 111 | + scope.setUser({ ip_address: ctx.clientAddress }); |
89 | 112 | });
|
| 113 | + } |
90 | 114 |
|
91 |
| - try { |
92 |
| - // storing res in a variable instead of directly returning is necessary to |
93 |
| - // invoke the catch block if next() throws |
94 |
| - const res = await startSpan( |
95 |
| - { |
96 |
| - name: `${method} ${interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params)}`, |
97 |
| - op: 'http.server', |
98 |
| - origin: 'auto.http.astro', |
99 |
| - status: 'ok', |
100 |
| - ...traceparentData, |
101 |
| - metadata: { |
102 |
| - source: 'route', |
103 |
| - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, |
104 |
| - }, |
105 |
| - data: { |
106 |
| - method, |
107 |
| - url: stripUrlQueryAndFragment(ctx.url.href), |
108 |
| - ...(ctx.url.search && { 'http.query': ctx.url.search }), |
109 |
| - ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), |
110 |
| - ...(options.trackHeaders && { headers: allHeaders }), |
111 |
| - }, |
| 115 | + try { |
| 116 | + // storing res in a variable instead of directly returning is necessary to |
| 117 | + // invoke the catch block if next() throws |
| 118 | + const res = await startSpan( |
| 119 | + { |
| 120 | + ...traceCtx, |
| 121 | + name: `${method} ${interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params)}`, |
| 122 | + op: 'http.server', |
| 123 | + origin: 'auto.http.astro', |
| 124 | + status: 'ok', |
| 125 | + metadata: { |
| 126 | + ...traceCtx?.metadata, |
| 127 | + source: 'route', |
112 | 128 | },
|
113 |
| - async span => { |
114 |
| - const originalResponse = await next(); |
115 |
| - |
116 |
| - if (span && originalResponse.status) { |
117 |
| - span.setHttpStatus(originalResponse.status); |
118 |
| - } |
119 |
| - |
120 |
| - const hub = getCurrentHub(); |
121 |
| - const client = hub.getClient(); |
122 |
| - const contentType = originalResponse.headers.get('content-type'); |
123 |
| - |
124 |
| - const isPageloadRequest = contentType && contentType.startsWith('text/html'); |
125 |
| - if (!isPageloadRequest || !client) { |
126 |
| - return originalResponse; |
127 |
| - } |
128 |
| - |
129 |
| - // Type case necessary b/c the body's ReadableStream type doesn't include |
130 |
| - // the async iterator that is actually available in Node |
131 |
| - // We later on use the async iterator to read the body chunks |
132 |
| - // see https://github.com/microsoft/TypeScript/issues/39051 |
133 |
| - const originalBody = originalResponse.body as NodeJS.ReadableStream | null; |
134 |
| - if (!originalBody) { |
135 |
| - return originalResponse; |
136 |
| - } |
137 |
| - |
138 |
| - const newResponseStream = new ReadableStream({ |
139 |
| - start: async controller => { |
140 |
| - for await (const chunk of originalBody) { |
141 |
| - const html = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk); |
142 |
| - const modifiedHtml = addMetaTagToHead(html, hub, span); |
143 |
| - controller.enqueue(new TextEncoder().encode(modifiedHtml)); |
144 |
| - } |
145 |
| - controller.close(); |
146 |
| - }, |
147 |
| - }); |
148 |
| - |
149 |
| - return new Response(newResponseStream, originalResponse); |
| 129 | + data: { |
| 130 | + method, |
| 131 | + url: stripUrlQueryAndFragment(ctx.url.href), |
| 132 | + ...(ctx.url.search && { 'http.query': ctx.url.search }), |
| 133 | + ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), |
| 134 | + ...(options.trackHeaders && { headers: allHeaders }), |
150 | 135 | },
|
151 |
| - ); |
152 |
| - return res; |
153 |
| - } catch (e) { |
154 |
| - sendErrorToSentry(e); |
155 |
| - throw e; |
156 |
| - } |
157 |
| - // TODO: flush if serveless (first extract function) |
158 |
| - }; |
159 |
| -}; |
| 136 | + }, |
| 137 | + async span => { |
| 138 | + const originalResponse = await next(); |
| 139 | + |
| 140 | + if (span && originalResponse.status) { |
| 141 | + span.setHttpStatus(originalResponse.status); |
| 142 | + } |
| 143 | + |
| 144 | + const hub = getCurrentHub(); |
| 145 | + const client = hub.getClient(); |
| 146 | + const contentType = originalResponse.headers.get('content-type'); |
| 147 | + |
| 148 | + const isPageloadRequest = contentType && contentType.startsWith('text/html'); |
| 149 | + if (!isPageloadRequest || !client) { |
| 150 | + return originalResponse; |
| 151 | + } |
| 152 | + |
| 153 | + // Type case necessary b/c the body's ReadableStream type doesn't include |
| 154 | + // the async iterator that is actually available in Node |
| 155 | + // We later on use the async iterator to read the body chunks |
| 156 | + // see https://github.com/microsoft/TypeScript/issues/39051 |
| 157 | + const originalBody = originalResponse.body as NodeJS.ReadableStream | null; |
| 158 | + if (!originalBody) { |
| 159 | + return originalResponse; |
| 160 | + } |
| 161 | + |
| 162 | + const decoder = new TextDecoder(); |
| 163 | + |
| 164 | + const newResponseStream = new ReadableStream({ |
| 165 | + start: async controller => { |
| 166 | + for await (const chunk of originalBody) { |
| 167 | + const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk); |
| 168 | + const modifiedHtml = addMetaTagToHead(html, hub, span); |
| 169 | + controller.enqueue(new TextEncoder().encode(modifiedHtml)); |
| 170 | + } |
| 171 | + controller.close(); |
| 172 | + }, |
| 173 | + }); |
| 174 | + |
| 175 | + return new Response(newResponseStream, originalResponse); |
| 176 | + }, |
| 177 | + ); |
| 178 | + return res; |
| 179 | + } catch (e) { |
| 180 | + sendErrorToSentry(e); |
| 181 | + throw e; |
| 182 | + } |
| 183 | + // TODO: flush if serverless (first extract function) |
| 184 | +} |
160 | 185 |
|
161 | 186 | /**
|
162 | 187 | * This function optimistically assumes that the HTML coming in chunks will not be split
|
|
0 commit comments