Skip to content

Commit 7dfb346

Browse files
authored
Merge branch 'develop' into cg-optimize-dropundefinedkeys
2 parents 4c9c98e + f4cd540 commit 7dfb346

File tree

5 files changed

+74
-47
lines changed

5 files changed

+74
-47
lines changed

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

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import { stealthWrap } from './utils';
99
type Http = typeof http;
1010
type Https = typeof https;
1111

12+
// The reason this "before OTEL" integration even exists is due to timing reasons. We need to be able to register a
13+
// `res.on('close')` handler **after** OTEL registers its own handler (which it uses to end spans), so that we can do
14+
// something (ie. flush) after OTEL has ended a span for a request. If you think about it like an onion:
15+
//
16+
// (Sentry after OTEL instrumentation
17+
// (OTEL instrumentation
18+
// (Sentry before OTEL instrumentation
19+
// (orig HTTP request handler))))
20+
//
21+
// registering an instrumentation before OTEL allows us to do this for incoming requests.
22+
1223
/**
1324
* A Sentry specific http instrumentation that is applied before the otel instrumentation.
1425
*/
@@ -70,46 +81,50 @@ export class SentryHttpInstrumentationBeforeOtel extends InstrumentationBase {
7081
function patchResponseToFlushOnServerlessPlatforms(res: http.OutgoingMessage): void {
7182
// Freely extend this function with other platforms if necessary
7283
if (process.env.VERCEL) {
73-
let markOnEndDone = (): void => undefined;
74-
const onEndDonePromise = new Promise<void>(res => {
75-
markOnEndDone = res;
76-
});
77-
78-
res.on('close', () => {
79-
markOnEndDone();
80-
});
81-
82-
// eslint-disable-next-line @typescript-eslint/unbound-method
83-
res.end = new Proxy(res.end, {
84-
apply(target, thisArg, argArray) {
85-
vercelWaitUntil(
86-
new Promise<void>(finishWaitUntil => {
87-
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
88-
const timeout = setTimeout(() => {
89-
finishWaitUntil();
90-
}, 2000);
91-
92-
onEndDonePromise
93-
.then(() => {
94-
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
95-
return flush(2000);
96-
})
97-
.then(
98-
() => {
99-
clearTimeout(timeout);
100-
finishWaitUntil();
101-
},
102-
e => {
103-
clearTimeout(timeout);
104-
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
105-
finishWaitUntil();
106-
},
107-
);
108-
}),
109-
);
110-
111-
return target.apply(thisArg, argArray);
112-
},
113-
});
84+
// In some cases res.end does not seem to be defined leading to errors if passed to Proxy
85+
// https://github.com/getsentry/sentry-javascript/issues/15759
86+
if (typeof res.end === 'function') {
87+
let markOnEndDone = (): void => undefined;
88+
const onEndDonePromise = new Promise<void>(res => {
89+
markOnEndDone = res;
90+
});
91+
92+
res.on('close', () => {
93+
markOnEndDone();
94+
});
95+
96+
// eslint-disable-next-line @typescript-eslint/unbound-method
97+
res.end = new Proxy(res.end, {
98+
apply(target, thisArg, argArray) {
99+
vercelWaitUntil(
100+
new Promise<void>(finishWaitUntil => {
101+
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
102+
const timeout = setTimeout(() => {
103+
finishWaitUntil();
104+
}, 2000);
105+
106+
onEndDonePromise
107+
.then(() => {
108+
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
109+
return flush(2000);
110+
})
111+
.then(
112+
() => {
113+
clearTimeout(timeout);
114+
finishWaitUntil();
115+
},
116+
e => {
117+
clearTimeout(timeout);
118+
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
119+
finishWaitUntil();
120+
},
121+
);
122+
}),
123+
);
124+
125+
return target.apply(thisArg, argArray);
126+
},
127+
});
128+
}
114129
}
115130
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,17 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
142142
return {
143143
name: INTEGRATION_NAME,
144144
setupOnce() {
145-
instrumentSentryHttpBeforeOtel();
145+
// Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific.
146+
// Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL
147+
// instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation
148+
// _after_ we apply OTEL instrumentation (e.g. for isolation scope handling and breadcrumbs).
149+
150+
// This is Sentry-specific instrumentation that is applied _before_ any OTEL instrumentation.
151+
if (process.env.VERCEL) {
152+
// Currently this instrumentation only does something when deployed on Vercel, so to save some overhead, we short circuit adding it here only for Vercel.
153+
// If it's functionality is extended in the future, feel free to remove the if statement and this comment.
154+
instrumentSentryHttpBeforeOtel();
155+
}
146156

147157
const instrumentSpans = _shouldInstrumentSpans(options, getClient<NodeClient>()?.getOptions());
148158

@@ -152,9 +162,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
152162
instrumentOtelHttp(instrumentationConfig);
153163
}
154164

155-
// This is the Sentry-specific instrumentation that isolates requests & creates breadcrumbs
156-
// Note that this _has_ to be wrapped after the OTEL instrumentation,
157-
// otherwise the isolation will not work correctly
165+
// This is Sentry-specific instrumentation that is applied _after_ any OTEL instrumentation.
158166
instrumentSentryHttp({
159167
...options,
160168
// If spans are not instrumented, it means the HttpInstrumentation has not been added

packages/profiling-node/src/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ class ContinuousProfiler {
516516
const metadata = this._client.getSdkMetadata();
517517
const tunnel = this._client.getOptions().tunnel;
518518

519-
const envelope = makeProfileChunkEnvelope(chunk, metadata?.sdk, tunnel, dsn);
519+
const envelope = makeProfileChunkEnvelope('node', chunk, metadata?.sdk, tunnel, dsn);
520520
transport.send(envelope).then(null, reason => {
521521
DEBUG_BUILD && logger.error('Error while sending profile chunk envelope:', reason);
522522
});

packages/profiling-node/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,13 +400,15 @@ export function createEventEnvelopeHeaders(
400400
* Creates a standalone profile_chunk envelope.
401401
*/
402402
export function makeProfileChunkEnvelope(
403+
platform: 'node',
403404
chunk: ProfileChunk,
404405
sdkInfo: SdkInfo | undefined,
405406
tunnel: string | undefined,
406407
dsn?: DsnComponents,
407408
): ProfileChunkEnvelope {
408409
const profileChunkHeader: ProfileChunkItem[0] = {
409410
type: 'profile_chunk',
411+
platform,
410412
};
411413

412414
return createEnvelope<ProfileChunkEnvelope>(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [

packages/profiling-node/test/integration.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,9 @@ describe('ProfilingIntegration', () => {
643643
Sentry.profiler.stopProfiler();
644644
vi.advanceTimersByTime(1000);
645645

646-
expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]?.type).toBe('profile_chunk');
646+
const envelopeHeaders = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0];
647+
expect(envelopeHeaders?.type).toBe('profile_chunk');
648+
expect(envelopeHeaders?.platform).toBe('node');
647649
});
648650

649651
it('sets global profile context', async () => {

0 commit comments

Comments
 (0)