Skip to content

Commit 96423b2

Browse files
committed
ref(node): Avoid double wrapping http module for vercel-edge
1 parent 2e41f5e commit 96423b2

File tree

3 files changed

+79
-152
lines changed

3 files changed

+79
-152
lines changed

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ import { context, propagation } from '@opentelemetry/api';
88
import { VERSION } from '@opentelemetry/core';
99
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
1010
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
11-
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
12-
import {
13-
addBreadcrumb,
11+
import type { AggregationCounts, Client, SanitizedRequestData, Scope} from '@sentry/core';
12+
import { addBreadcrumb,
1413
addNonEnumerableProperty,
15-
generateSpanId,
14+
flush, generateSpanId,
1615
getBreadcrumbLogLevelFromHttpStatusCode,
1716
getClient,
1817
getCurrentScope,
@@ -22,6 +21,7 @@ import {
2221
logger,
2322
parseUrl,
2423
stripUrlQueryAndFragment,
24+
vercelWaitUntil ,
2525
withIsolationScope,
2626
} from '@sentry/core';
2727
import { DEBUG_BUILD } from '../../debug-build';
@@ -127,6 +127,11 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
127127
this._onOutgoingRequestFinish(data.request, undefined);
128128
}) satisfies ChannelListener;
129129

130+
const onHttpServerResponseCreated = ((_data: unknown) => {
131+
const data = _data as { response: http.OutgoingMessage };
132+
patchResponseToFlushOnServerlessPlatformsOnce(data.response);
133+
}) satisfies ChannelListener;
134+
130135
/**
131136
* You may be wondering why we register these diagnostics-channel listeners
132137
* in such a convoluted way (as InstrumentationNodeModuleDefinition...)˝,
@@ -153,6 +158,11 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
153158
// In this case, `http.client.response.finish` is not triggered
154159
subscribe('http.client.request.error', onHttpClientRequestError);
155160

161+
// On vercel, ensure that we flush events before the lambda freezes
162+
if (process.env.VERCEL) {
163+
subscribe('http.server.response.created', onHttpServerResponseCreated);
164+
}
165+
156166
return moduleExports;
157167
},
158168
() => {
@@ -178,6 +188,11 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
178188
// In this case, `http.client.response.finish` is not triggered
179189
subscribe('http.client.request.error', onHttpClientRequestError);
180190

191+
// On vercel, ensure that we flush events before the lambda freezes
192+
if (process.env.VERCEL) {
193+
subscribe('http.server.response.created', onHttpServerResponseCreated);
194+
}
195+
181196
return moduleExports;
182197
},
183198
() => {
@@ -529,6 +544,66 @@ export function recordRequestSession({
529544
});
530545
}
531546

547+
function patchResponseToFlushOnServerlessPlatformsOnce(res: http.OutgoingMessage): void {
548+
// This means it was already patched, do nothing
549+
if ((res as { __sentry_patched__?: boolean }).__sentry_patched__) {
550+
return;
551+
}
552+
553+
DEBUG_BUILD && logger.log(INSTRUMENTATION_NAME, 'Patching server.end()');
554+
addNonEnumerableProperty(res, '__sentry_patched__', true);
555+
556+
// This is vercel specific handling to flush events before the lambda freezes
557+
558+
// In some cases res.end does not seem to be defined leading to errors if passed to Proxy
559+
// https://github.com/getsentry/sentry-javascript/issues/15759
560+
if (typeof res.end !== 'function') {
561+
return;
562+
}
563+
564+
let markOnEndDone = (): void => undefined;
565+
const onEndDonePromise = new Promise<void>(res => {
566+
markOnEndDone = res;
567+
});
568+
569+
res.on('close', () => {
570+
markOnEndDone();
571+
});
572+
573+
// eslint-disable-next-line @typescript-eslint/unbound-method
574+
res.end = new Proxy(res.end, {
575+
apply(target, thisArg, argArray) {
576+
vercelWaitUntil(
577+
new Promise<void>(finishWaitUntil => {
578+
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
579+
const timeout = setTimeout(() => {
580+
finishWaitUntil();
581+
}, 2000);
582+
583+
onEndDonePromise
584+
.then(() => {
585+
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
586+
return flush(2000);
587+
})
588+
.then(
589+
() => {
590+
clearTimeout(timeout);
591+
finishWaitUntil();
592+
},
593+
e => {
594+
clearTimeout(timeout);
595+
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
596+
finishWaitUntil();
597+
},
598+
);
599+
}),
600+
);
601+
602+
return target.apply(thisArg, argArray);
603+
},
604+
});
605+
}
606+
532607
const clientToRequestSessionAggregatesMap = new Map<
533608
Client,
534609
{ [timestampRoundedToSeconds: string]: { exited: number; crashed: number; errored: number } }

packages/node/src/integrations/http/SentryHttpInstrumentationBeforeOtel.ts

Lines changed: 0 additions & 130 deletions
This file was deleted.

packages/node/src/integrations/http/index.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { addOriginToSpan } from '../../utils/addOriginToSpan';
1212
import { getRequestUrl } from '../../utils/getRequestUrl';
1313
import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation';
1414
import { SentryHttpInstrumentation } from './SentryHttpInstrumentation';
15-
import { SentryHttpInstrumentationBeforeOtel } from './SentryHttpInstrumentationBeforeOtel';
1615

1716
const INTEGRATION_NAME = 'Http';
1817

@@ -108,10 +107,6 @@ interface HttpOptions {
108107
};
109108
}
110109

111-
const instrumentSentryHttpBeforeOtel = generateInstrumentOnce(`${INTEGRATION_NAME}.sentry-before-otel`, () => {
112-
return new SentryHttpInstrumentationBeforeOtel();
113-
});
114-
115110
const instrumentSentryHttp = generateInstrumentOnce<SentryHttpInstrumentationOptions>(
116111
`${INTEGRATION_NAME}.sentry`,
117112
options => {
@@ -151,19 +146,6 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
151146
return {
152147
name: INTEGRATION_NAME,
153148
setupOnce() {
154-
// TODO: get rid of this too
155-
// Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific.
156-
// Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL
157-
// instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation
158-
// _after_ we apply OTEL instrumentation (e.g. for isolation scope handling and breadcrumbs).
159-
160-
// This is Sentry-specific instrumentation that is applied _before_ any OTEL instrumentation.
161-
if (process.env.VERCEL) {
162-
// Currently this instrumentation only does something when deployed on Vercel, so to save some overhead, we short circuit adding it here only for Vercel.
163-
// If it's functionality is extended in the future, feel free to remove the if statement and this comment.
164-
instrumentSentryHttpBeforeOtel();
165-
}
166-
167149
const instrumentSpans = _shouldInstrumentSpans(options, getClient<NodeClient>()?.getOptions());
168150

169151
// This is Sentry-specific instrumentation for request isolation and breadcrumbs

0 commit comments

Comments
 (0)