Skip to content

Commit 660e15f

Browse files
committed
fix(cloudflare): Missing events inside waitUntil
1 parent 0591b1b commit 660e15f

File tree

3 files changed

+61
-4
lines changed

3 files changed

+61
-4
lines changed

dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export default Sentry.withSentry(
8181
},
8282
}),
8383
{
84-
async fetch(request, env) {
84+
async fetch(request, env, ctx) {
8585
const url = new URL(request.url);
8686
switch (url.pathname) {
8787
case '/rpc/throwException':
@@ -96,6 +96,20 @@ export default Sentry.withSentry(
9696
}
9797
}
9898
break;
99+
case '/waitUntil':
100+
console.log('waitUntil called');
101+
102+
const longRunningTask = async () => {
103+
await new Promise(resolve => setTimeout(resolve, 3000));
104+
105+
console.log('ʕっ•ᴥ•ʔっ');
106+
Sentry.captureException(new Error('ʕノ•ᴥ•ʔノ ︵ ┻━┻'));
107+
};
108+
109+
ctx.waitUntil(longRunningTask());
110+
111+
return new Response(null, { status: 200 });
112+
99113
case '/throwException':
100114
throw new Error('To be recorded in Sentry.');
101115
default:

dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForRequest } from '@sentry-internal/test-utils';
2+
import { waitForError, waitForRequest, waitForTransaction } from '@sentry-internal/test-utils';
33
import { SDK_VERSION } from '@sentry/cloudflare';
4+
import { waitForDebugger } from 'inspector';
45
import { WebSocket } from 'ws';
56

67
test('Index page', async ({ baseURL }) => {
@@ -82,3 +83,33 @@ test('sends user-agent header with SDK name and version in envelope requests', a
8283
'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`,
8384
});
8485
});
86+
87+
test.only('waitUntil', async ({ baseURL }) => {
88+
const errorWaiter = waitForError('cloudflare-workers', () => true);
89+
const waitUntilSpanWaiter = waitForTransaction('cloudflare-workers', span => span.transaction === 'waitUntil');
90+
const httpTransactionWaiter = waitForTransaction(
91+
'cloudflare-workers',
92+
transactionEvent => transactionEvent.contexts?.trace?.op === 'http.server',
93+
);
94+
95+
const response = await fetch(`${baseURL}/waitUntil`);
96+
97+
const transactionRequest = await httpTransactionWaiter;
98+
const waitUntilSpan = await waitUntilSpanWaiter;
99+
const errorEvent = await errorWaiter;
100+
101+
expect(response.status).toBe(200);
102+
103+
// All traceIds should be the same
104+
expect(transactionRequest.contexts?.trace?.trace_id).toBe(waitUntilSpan.contexts?.trace?.trace_id);
105+
expect(transactionRequest.contexts?.trace?.trace_id).toBe(errorEvent.contexts?.trace?.trace_id);
106+
107+
expect(errorEvent.exception?.values?.[0]?.value).toBe('ʕノ•ᴥ•ʔノ ︵ ┻━┻');
108+
expect(errorEvent.breadcrumbs).toStrictEqual(waitUntilSpan.breadcrumbs);
109+
110+
expect(waitUntilSpan.contexts?.trace?.parent_span_id).toBe(transactionRequest.contexts?.trace?.span_id);
111+
112+
console.log(JSON.stringify(transactionRequest, null, 2));
113+
console.log(JSON.stringify(waitUntilSpan, null, 2));
114+
console.log(JSON.stringify(errorEvent, null, 2));
115+
});

packages/cloudflare/src/flush.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ExecutionContext } from '@cloudflare/workers-types';
2+
import { startSpan, withScope } from '@sentry/core';
23

34
type FlushLock = {
45
readonly ready: Promise<void>;
@@ -22,9 +23,20 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
2223
const originalWaitUntil = context.waitUntil.bind(context) as typeof context.waitUntil;
2324
context.waitUntil = promise => {
2425
pending++;
26+
2527
return originalWaitUntil(
26-
promise.finally(() => {
27-
if (--pending === 0) resolveAllDone();
28+
// Wrap the promise in a new scope and transaction so spans created inside
29+
// waitUntil callbacks are properly isolated from the HTTP request transaction
30+
withScope(() =>
31+
startSpan({ forceTransaction: true, op: 'cloudflare.wait_until', name: 'wait_until' }, async () => {
32+
// By awaiting the promise inside the new scope, all of its continuations
33+
// will execute in this isolated scope
34+
await promise;
35+
}),
36+
).finally(() => {
37+
if (--pending === 0) {
38+
resolveAllDone();
39+
}
2840
}),
2941
);
3042
};

0 commit comments

Comments
 (0)