Skip to content

Commit 67ee12d

Browse files
committed
ref(node): Avoid double wrapping http module for vercel-edge
1 parent 72ddee3 commit 67ee12d

File tree

4 files changed

+66
-191
lines changed

4 files changed

+66
-191
lines changed

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

+64-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
/* eslint-disable max-lines */
12
import { subscribe } from 'node:diagnostics_channel';
23
import type * as http from 'node:http';
34
import type { EventEmitter } from 'node:stream';
45
import { context, propagation } from '@opentelemetry/api';
56
import { VERSION } from '@opentelemetry/core';
67
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
78
import { InstrumentationBase } from '@opentelemetry/instrumentation';
8-
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
9-
import {
10-
addBreadcrumb,
9+
import type { AggregationCounts, Client, SanitizedRequestData, Scope} from '@sentry/core';
10+
import { addBreadcrumb,
1111
addNonEnumerableProperty,
12-
generateSpanId,
12+
flush, generateSpanId,
1313
getBreadcrumbLogLevelFromHttpStatusCode,
1414
getClient,
1515
getCurrentScope,
@@ -19,6 +19,7 @@ import {
1919
logger,
2020
parseUrl,
2121
stripUrlQueryAndFragment,
22+
vercelWaitUntil ,
2223
withIsolationScope,
2324
} from '@sentry/core';
2425
import { DEBUG_BUILD } from '../../debug-build';
@@ -114,6 +115,14 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
114115
this._onOutgoingRequestFinish(request, response);
115116
});
116117

118+
// On vercel, ensure that we flush events before the lambda freezes
119+
if (process.env.VERCEL) {
120+
subscribe('http.server.response.created', data => {
121+
const response = (data as { response: http.ServerResponse }).response;
122+
patchResponseToFlushOnServerlessPlatforms(response);
123+
});
124+
}
125+
117126
return [];
118127
}
119128

@@ -458,3 +467,54 @@ const clientToRequestSessionAggregatesMap = new Map<
458467
Client,
459468
{ [timestampRoundedToSeconds: string]: { exited: number; crashed: number; errored: number } }
460469
>();
470+
471+
function patchResponseToFlushOnServerlessPlatforms(res: http.OutgoingMessage): void {
472+
// Freely extend this function with other platforms if necessary
473+
if (process.env.VERCEL) {
474+
// In some cases res.end does not seem to be defined leading to errors if passed to Proxy
475+
// https://github.com/getsentry/sentry-javascript/issues/15759
476+
if (typeof res.end === 'function') {
477+
let markOnEndDone = (): void => undefined;
478+
const onEndDonePromise = new Promise<void>(res => {
479+
markOnEndDone = res;
480+
});
481+
482+
res.on('close', () => {
483+
markOnEndDone();
484+
});
485+
486+
// eslint-disable-next-line @typescript-eslint/unbound-method
487+
res.end = new Proxy(res.end, {
488+
apply(target, thisArg, argArray) {
489+
vercelWaitUntil(
490+
new Promise<void>(finishWaitUntil => {
491+
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
492+
const timeout = setTimeout(() => {
493+
finishWaitUntil();
494+
}, 2000);
495+
496+
onEndDonePromise
497+
.then(() => {
498+
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
499+
return flush(2000);
500+
})
501+
.then(
502+
() => {
503+
clearTimeout(timeout);
504+
finishWaitUntil();
505+
},
506+
e => {
507+
clearTimeout(timeout);
508+
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
509+
finishWaitUntil();
510+
},
511+
);
512+
}),
513+
);
514+
515+
return target.apply(thisArg, argArray);
516+
},
517+
});
518+
}
519+
}
520+
}

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

-130
This file was deleted.

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

+2-18
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,18 +146,6 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
151146
return {
152147
name: INTEGRATION_NAME,
153148
setupOnce() {
154-
// Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific.
155-
// Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL
156-
// instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation
157-
// _after_ we apply OTEL instrumentation (e.g. for isolation scope handling and breadcrumbs).
158-
159-
// This is Sentry-specific instrumentation that is applied _before_ any OTEL instrumentation.
160-
if (process.env.VERCEL) {
161-
// Currently this instrumentation only does something when deployed on Vercel, so to save some overhead, we short circuit adding it here only for Vercel.
162-
// If it's functionality is extended in the future, feel free to remove the if statement and this comment.
163-
instrumentSentryHttpBeforeOtel();
164-
}
165-
166149
const instrumentSpans = _shouldInstrumentSpans(options, getClient<NodeClient>()?.getOptions());
167150

168151
// This is the "regular" OTEL instrumentation that emits spans
@@ -171,7 +154,8 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
171154
instrumentOtelHttp(instrumentationConfig);
172155
}
173156

174-
// This is Sentry-specific instrumentation that is applied _after_ any OTEL instrumentation.
157+
// This is Sentry-specific instrumentation
158+
// It uses diagnostics channel, so it does not rely on import-in-the-middle or similar shenanigans
175159
instrumentSentryHttp({
176160
...options,
177161
// If spans are not instrumented, it means the HttpInstrumentation has not been added

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

-39
This file was deleted.

0 commit comments

Comments
 (0)