From 9b45d97dbe5784ea1604fc4cf4d04acdc7b57967 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 11 Dec 2025 13:39:11 +0100 Subject: [PATCH 1/3] fix(cloudflare): Missing events inside waitUntil --- .../cloudflare-workers/src/index.ts | 21 ++- .../cloudflare-workers/tests/index.test.ts | 102 +++++++++++++- packages/cloudflare/src/client.ts | 16 ++- packages/cloudflare/src/flush.ts | 14 +- packages/cloudflare/src/request.ts | 125 +++++++++--------- .../src/utils/endSpanAfterWaitUntil.ts | 17 +++ 6 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 packages/cloudflare/src/utils/endSpanAfterWaitUntil.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts index ab438432a004..5534aeb486e3 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts @@ -81,7 +81,7 @@ export default Sentry.withSentry( }, }), { - async fetch(request, env) { + async fetch(request, env, ctx) { const url = new URL(request.url); switch (url.pathname) { case '/rpc/throwException': @@ -96,6 +96,25 @@ export default Sentry.withSentry( } } break; + case '/waitUntil': + console.log('waitUntil called'); + + const longRunningTask = async () => { + await new Promise(resolve => setTimeout(resolve, 2000)); + + console.log('ʕっ•ᴥ•ʔっ'); + Sentry.captureException(new Error('ʕノ•ᴥ•ʔノ ︵ ┻━┻')); + + return Sentry.startSpan({ name: 'longRunningTask' }, async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log(' /|\ ^._.^ /|\ '); + }); + }; + + ctx.waitUntil(longRunningTask()); + + return new Response(null, { status: 200 }); + case '/throwException': throw new Error('To be recorded in Sentry.'); default: diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts index 8c09693c81ed..f3252caede5e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForRequest } from '@sentry-internal/test-utils'; +import { waitForError, waitForRequest, waitForTransaction } from '@sentry-internal/test-utils'; import { SDK_VERSION } from '@sentry/cloudflare'; import { WebSocket } from 'ws'; @@ -82,3 +82,103 @@ test('sends user-agent header with SDK name and version in envelope requests', a 'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`, }); }); + +test.only('waitUntil', async ({ baseURL }) => { + const errorWaiter = waitForError( + 'cloudflare-workers', + event => event.exception?.values?.[0]?.value === 'ʕノ•ᴥ•ʔノ ︵ ┻━┻', + ); + const httpTransactionWaiter = waitForTransaction( + 'cloudflare-workers', + transactionEvent => transactionEvent.contexts?.trace?.op === 'http.server', + ); + + const response = await fetch(`${baseURL}/waitUntil`); + + expect(response.status).toBe(200); + + const [errorEvent, transactionEvent] = await Promise.all([errorWaiter, httpTransactionWaiter]); + + // ===== Error Event Assertions ===== + expect(errorEvent.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: 'ʕノ•ᴥ•ʔノ ︵ ┻━┻', + mechanism: { + type: 'generic', + handled: true, + }, + }); + + // Error should have trace context linking it to the transaction + expect(errorEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(errorEvent.contexts?.trace?.span_id).toBeDefined(); + + // Error should have cloudflare-specific contexts + expect(errorEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' }); + expect(errorEvent.contexts?.runtime).toEqual({ name: 'cloudflare' }); + + // Error should have request data + expect(errorEvent.request).toMatchObject({ + method: 'GET', + url: expect.stringContaining('/waitUntil'), + }); + + // Error should have console breadcrumbs from before the error + expect(errorEvent.breadcrumbs).toEqual([ + expect.objectContaining({ category: 'console', message: 'waitUntil called' }), + expect.objectContaining({ category: 'console', message: 'ʕっ•ᴥ•ʔっ' }), + ]); + + // ===== Transaction Event Assertions ===== + expect(transactionEvent.transaction).toBe('GET /waitUntil'); + expect(transactionEvent.type).toBe('transaction'); + expect(transactionEvent.transaction_info?.source).toBe('url'); + + // Transaction trace context (root span - no status/response code, those are on the fetch child span) + expect(transactionEvent.contexts?.trace).toMatchObject({ + op: 'http.server', + status: 'ok', + origin: 'auto.http.cloudflare', + data: expect.objectContaining({ + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.cloudflare', + 'http.request.method': 'GET', + 'url.path': '/waitUntil', + 'http.response.status_code': 200, + }), + }); + + expect(transactionEvent.spans).toEqual([ + expect.objectContaining({ + description: 'fetch', + op: 'http.server', + origin: 'auto.http.cloudflare', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + }), + expect.objectContaining({ + description: 'waitUntil', + op: 'cloudflare.wait_until', + origin: 'manual', + parent_span_id: transactionEvent.spans?.[0]?.span_id, + }), + expect.objectContaining({ + description: 'longRunningTask', + origin: 'manual', + parent_span_id: transactionEvent.spans?.[0]?.span_id, + }), + ]); + + // Transaction should have all console breadcrumbs including the one after the span completes + expect(transactionEvent.breadcrumbs).toEqual([ + expect.objectContaining({ category: 'console', message: 'waitUntil called' }), + expect.objectContaining({ category: 'console', message: 'ʕっ•ᴥ•ʔっ' }), + expect.objectContaining({ category: 'console', message: ' /|\ ^._.^ /|\ ' }), + ]); + + // ===== Cross-event Assertions ===== + // Error and transaction should share the same trace_id + expect(transactionEvent.contexts?.trace?.trace_id).toBe(errorEvent.contexts?.trace?.trace_id); + + // The error's span_id should match the fetch span's span_id (error captured during waitUntil execution) + expect(errorEvent.contexts?.trace?.span_id).toBe(transactionEvent.spans?.[0]?.span_id); +}); diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 3332f71dab90..9d0bf63a2d01 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -63,6 +63,18 @@ export class CloudflareClient extends ServerRuntimeClient { }); } + /** + * Returns a promise that resolves when all waitUntil promises have completed. + * This allows the root span to stay open until all waitUntil work is done. + * + * @return {Promise} A promise that resolves when all waitUntil promises are done. + */ + public async waitUntilDone(): Promise { + if (this._flushLock) { + await this._flushLock.finalize(); + } + } + /** * Flushes pending operations and ensures all data is processed. * If a timeout is provided, the operation will be completed within the specified time limit. @@ -73,9 +85,7 @@ export class CloudflareClient extends ServerRuntimeClient { * @return {Promise} A promise that resolves to a boolean indicating whether the flush operation was successful. */ public async flush(timeout?: number): Promise { - if (this._flushLock) { - await this._flushLock.finalize(); - } + await this.waitUntilDone(); if (this._pendingSpans.size > 0 && this._spanCompletionPromise) { DEBUG_BUILD && diff --git a/packages/cloudflare/src/flush.ts b/packages/cloudflare/src/flush.ts index f38c805d0f8b..b524be1c0b78 100644 --- a/packages/cloudflare/src/flush.ts +++ b/packages/cloudflare/src/flush.ts @@ -1,4 +1,5 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; +import { startSpan } from '@sentry/core'; type FlushLock = { readonly ready: Promise; @@ -22,9 +23,18 @@ export function makeFlushLock(context: ExecutionContext): FlushLock { const originalWaitUntil = context.waitUntil.bind(context) as typeof context.waitUntil; context.waitUntil = promise => { pending++; + return originalWaitUntil( - promise.finally(() => { - if (--pending === 0) resolveAllDone(); + // Wrap the promise in a new scope and transaction so spans created inside + // waitUntil callbacks are properly isolated from the HTTP request transaction + startSpan({ op: 'cloudflare.wait_until', name: 'waitUntil' }, async () => { + // By awaiting the promise inside the new scope, all of its continuations + // will execute in this isolated scope + await promise; + }).finally(() => { + if (--pending === 0) { + resolveAllDone(); + } }), ); }; diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index c404e57d01d8..42b04655e7d2 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -16,6 +16,7 @@ import { import type { CloudflareOptions } from './client'; import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; import { init } from './sdk'; +import { endSpanAfterWaitUntil } from './utils/endSpanAfterWaitUntil'; import { classifyResponseStreaming } from './utils/streaming'; interface RequestHandlerWrapperOptions { @@ -107,73 +108,79 @@ export function wrapRequestHandler( // See: https://developers.cloudflare.com/workers/runtime-apis/performance/ // Use startSpanManual to control when span ends (needed for streaming responses) - return startSpanManual({ name, attributes }, async span => { - let res: Response; - - try { - res = await handler(); - setHttpStatus(span, res.status); - - // After the handler runs, the span name might have been updated by nested instrumentation - // (e.g., Remix parameterizing routes). The span should already have the correct name - // from that instrumentation, so we don't need to do anything here. - } catch (e) { - span.end(); - if (captureErrors) { - captureException(e, { mechanism: { handled: false, type: 'auto.http.cloudflare' } }); - } - waitUntil?.(flush(2000)); - throw e; - } + return startSpanManual({ name, attributes }, async rootSpan => { + return startSpanManual({ name: 'fetch', attributes }, async fetchSpan => { + const finishSpansAndWaitUntil = (): void => { + fetchSpan.end(); + waitUntil?.(flush(2000)); + waitUntil?.(endSpanAfterWaitUntil(rootSpan)); + }; - // Classify response to detect actual streaming - const classification = classifyResponseStreaming(res); + let res: Response; - if (classification.isStreaming && res.body) { - // Streaming response detected - monitor consumption to keep span alive try { - const [clientStream, monitorStream] = res.body.tee(); + res = await handler(); + setHttpStatus(rootSpan, res.status); - // Monitor stream consumption and end span when complete - const streamMonitor = (async () => { - const reader = monitorStream.getReader(); + // After the handler runs, the span name might have been updated by nested instrumentation + // (e.g., Remix parameterizing routes). The span should already have the correct name + // from that instrumentation, so we don't need to do anything here. + } catch (e) { + // For errors, we still wait for waitUntil promises before ending the span + // so that any spans created in waitUntil callbacks are captured + if (captureErrors) { + captureException(e, { mechanism: { handled: false, type: 'auto.http.cloudflare' } }); + } + finishSpansAndWaitUntil(); + throw e; + } - try { - let done = false; - while (!done) { - const result = await reader.read(); - done = result.done; + // Classify response to detect actual streaming + const classification = classifyResponseStreaming(res); + + if (classification.isStreaming && res.body) { + // Streaming response detected - monitor consumption to keep span alive + try { + const [clientStream, monitorStream] = res.body.tee(); + + // Monitor stream consumption and end span when complete + const streamMonitor = (async () => { + const reader = monitorStream.getReader(); + + try { + let done = false; + while (!done) { + const result = await reader.read(); + done = result.done; + } + } catch { + // Stream error or cancellation - will end span in finally + } finally { + reader.releaseLock(); + finishSpansAndWaitUntil(); } - } catch { - // Stream error or cancellation - will end span in finally - } finally { - reader.releaseLock(); - span.end(); - waitUntil?.(flush(2000)); - } - })(); - - // Keep worker alive until stream monitoring completes (otherwise span won't end) - waitUntil?.(streamMonitor); - - // Return response with client stream - return new Response(clientStream, { - status: res.status, - statusText: res.statusText, - headers: res.headers, - }); - } catch (e) { - // tee() failed (e.g stream already locked) - fall back to non-streaming handling - span.end(); - waitUntil?.(flush(2000)); - return res; + })(); + + // Keep worker alive until stream monitoring completes (otherwise span won't end) + waitUntil?.(streamMonitor); + + // Return response with client stream + return new Response(clientStream, { + status: res.status, + statusText: res.statusText, + headers: res.headers, + }); + } catch (e) { + // tee() failed (e.g stream already locked) - fall back to non-streaming handling + finishSpansAndWaitUntil(); + return res; + } } - } - // Non-streaming response - end span immediately and return original - span.end(); - waitUntil?.(flush(2000)); - return res; + // Non-streaming response - end span after all waitUntil promises complete + finishSpansAndWaitUntil(); + return res; + }); }); }, ); diff --git a/packages/cloudflare/src/utils/endSpanAfterWaitUntil.ts b/packages/cloudflare/src/utils/endSpanAfterWaitUntil.ts new file mode 100644 index 000000000000..69df17df10f7 --- /dev/null +++ b/packages/cloudflare/src/utils/endSpanAfterWaitUntil.ts @@ -0,0 +1,17 @@ +import { flush, getClient, type Span } from '@sentry/core'; +import type { CloudflareClient } from '../client'; + +/** + * Helper to end span after all waitUntil promises complete. + * This ensures spans created in waitUntil callbacks are captured in the same transaction. + */ +export const endSpanAfterWaitUntil = async (span: Span): Promise => { + const cloudflareClient = getClient(); + + if (cloudflareClient) { + await cloudflareClient.waitUntilDone(); + } + + span.end(); + await flush(2000); +}; From a5ede3790efccb2fb5489f1c549518bec7dc5e67 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 15 Dec 2025 10:55:27 +0100 Subject: [PATCH 2/3] fixup! fix(cloudflare): Missing events inside waitUntil --- packages/cloudflare/src/request.ts | 1 + packages/cloudflare/test/request.test.ts | 61 +++++++++++++++++++----- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 42b04655e7d2..b4a3968ec28b 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -121,6 +121,7 @@ export function wrapRequestHandler( try { res = await handler(); setHttpStatus(rootSpan, res.status); + setHttpStatus(fetchSpan, res.status); // After the handler runs, the span name might have been updated by nested instrumentation // (e.g., Remix parameterizing routes). The span should already have the correct name diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 94b5d89e4ae0..6c1cc3cea327 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -18,6 +18,13 @@ function addDelayedWaitUntil(context: ExecutionContext) { context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); } +function createMockExecutionContext(): ExecutionContext { + return { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + }; +} + describe('withSentry', () => { beforeAll(() => { setAsyncLocalStorageAsyncContextStrategy(); @@ -46,7 +53,7 @@ describe('withSentry', () => { () => new Response('test'), ); - expect(waitUntilSpy).toHaveBeenCalledTimes(1); + expect(waitUntilSpy).toHaveBeenCalledTimes(3); expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise)); }); @@ -123,8 +130,10 @@ describe('withSentry', () => { const after = flushSpy.mock.calls.length; const delta = after - before; - // Verify that exactly one flush call was made during this test - expect(delta).toBe(1); + // Verify that two flush calls were made during this test + // One for the flush after the request handler is done + // and one for the waitUntil promise + expect(delta).toBe(2); }); describe('scope instrumentation', () => { @@ -285,12 +294,17 @@ describe('withSentry', () => { 'sentry-release=2.1.12,sentry-public_key=public,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=0.3232', ); + let sentryEventTransaction: Event = {}; let sentryEvent: Event = {}; await wrapRequestHandler( { options: { ...MOCK_OPTIONS, tracesSampleRate: 0, + beforeSendTransaction(event) { + sentryEventTransaction = event; + return null; + }, beforeSend(event) { sentryEvent = event; return null; @@ -304,8 +318,20 @@ describe('withSentry', () => { return new Response('test'); }, ); + + // Wait for async span end and transaction capture + await new Promise(resolve => setTimeout(resolve, 50)); + + expect(sentryEventTransaction.contexts?.trace).toEqual( + expect.objectContaining({ + parent_span_id: '1121201211212012', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: '12312012123120121231201212312012', + }), + ); + expect(sentryEvent.contexts?.trace).toEqual({ - parent_span_id: '1121201211212012', + parent_span_id: sentryEventTransaction.contexts?.trace?.span_id, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: '12312012123120121231201212312012', }); @@ -342,7 +368,25 @@ describe('withSentry', () => { await new Promise(resolve => setTimeout(resolve, 50)); expect(sentryEvent.transaction).toEqual('GET /'); - expect(sentryEvent.spans).toHaveLength(0); + expect(sentryEvent.spans).toHaveLength(1); + expect(sentryEvent.spans).toEqual([ + expect.objectContaining({ + data: expect.any(Object), + description: 'fetch', + op: 'http.server', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.cloudflare', + status: 'ok', + }), + ]); + expect(sentryEvent.contexts?.trace?.data).toStrictEqual({ + ...sentryEvent.spans?.[0]?.data, + 'sentry.sample_rate': 1, + }); expect(sentryEvent.contexts?.trace).toEqual({ data: { 'sentry.origin': 'auto.http.cloudflare', @@ -370,10 +414,3 @@ describe('withSentry', () => { }); }); }); - -function createMockExecutionContext(): ExecutionContext { - return { - waitUntil: vi.fn(), - passThroughOnException: vi.fn(), - }; -} From 456d334edfa38ef486dff3c0f347cb0bb2c8189d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 15 Dec 2025 13:20:14 +0100 Subject: [PATCH 3/3] fixup! fix(cloudflare): Missing events inside waitUntil --- .../cloudflare-mcp/tests/index.test.ts | 49 ++++++++++++++++--- .../cloudflare-workers/tests/index.test.ts | 17 +++++-- packages/cloudflare/src/request.ts | 1 - .../cloudflare/test/durableobject.test.ts | 6 ++- packages/cloudflare/test/handler.test.ts | 2 +- 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts index 8ce8b693499e..2112933da964 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/tests/index.test.ts @@ -49,7 +49,8 @@ test('sends spans for MCP tool calls', async ({ baseURL }) => { typeof mcpEvent === 'string' || !('contexts' in mcpEvent) || typeof requestEvent === 'string' || - !('contexts' in requestEvent) + !('contexts' in requestEvent) || + !('spans' in requestEvent) ) { throw new Error("Events don't have contexts"); } @@ -71,21 +72,55 @@ test('sends spans for MCP tool calls', async ({ baseURL }) => { 'url.port': '38787', 'url.scheme': 'http:', 'server.address': 'localhost', - 'http.request.body.size': 120, 'user_agent.original': 'node', - 'http.request.header.content_type': 'application/json', 'network.protocol.name': 'HTTP/1.1', - 'mcp.server.extra': ' /|\ ^._.^ /|\ ', - 'http.response.status_code': 200, }), op: 'http.server', - status: 'ok', origin: 'auto.http.cloudflare', }); + expect(requestEvent.spans).toEqual([ + { + data: { + 'sentry.origin': 'auto.http.cloudflare', + 'sentry.op': 'http.server', + 'sentry.source': 'url', + 'http.request.method': 'POST', + 'url.path': '/mcp', + 'url.full': 'http://localhost:38787/mcp', + 'url.port': '38787', + 'url.scheme': 'http:', + 'server.address': 'localhost', + 'http.request.body.size': 120, + 'user_agent.original': 'node', + 'http.request.header.accept': 'application/json, text/event-stream', + 'http.request.header.accept_encoding': 'br, gzip', + 'http.request.header.accept_language': '*', + 'http.request.header.cf_connecting_ip': '::1', + 'http.request.header.content_length': '120', + 'http.request.header.content_type': 'application/json', + 'http.request.header.host': 'localhost:38787', + 'http.request.header.sec_fetch_mode': 'cors', + 'http.request.header.user_agent': 'node', + 'network.protocol.name': 'HTTP/1.1', + 'mcp.server.extra': ' /|\ ^._.^ /|\ ', + 'http.response.status_code': 200, + }, + description: 'fetch', + op: 'http.server', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.cloudflare', + }, + ]); + expect(mcpEvent.contexts?.trace).toEqual({ trace_id: expect.any(String), - parent_span_id: requestEvent.contexts?.trace?.span_id, + parent_span_id: requestEvent.spans?.[0]?.span_id, span_id: expect.any(String), op: 'mcp.server', origin: 'auto.function.mcp_server', diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts index f3252caede5e..cb8b4b0db3bc 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts @@ -137,23 +137,34 @@ test.only('waitUntil', async ({ baseURL }) => { // Transaction trace context (root span - no status/response code, those are on the fetch child span) expect(transactionEvent.contexts?.trace).toMatchObject({ op: 'http.server', - status: 'ok', origin: 'auto.http.cloudflare', data: expect.objectContaining({ 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.cloudflare', - 'http.request.method': 'GET', 'url.path': '/waitUntil', - 'http.response.status_code': 200, }), }); + expect(transactionEvent.contexts?.trace).not.toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'http.request.method': 'GET', + 'http.response.status_code': 200, + }), + }), + ); + expect(transactionEvent.spans).toEqual([ expect.objectContaining({ + status: 'ok', description: 'fetch', op: 'http.server', origin: 'auto.http.cloudflare', parent_span_id: transactionEvent.contexts?.trace?.span_id, + data: expect.objectContaining({ + 'http.request.method': 'GET', + 'http.response.status_code': 200, + }), }), expect.objectContaining({ description: 'waitUntil', diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index b4a3968ec28b..ecda17d5b49a 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -120,7 +120,6 @@ export function wrapRequestHandler( try { res = await handler(); - setHttpStatus(rootSpan, res.status); setHttpStatus(fetchSpan, res.status); // After the handler runs, the span name might have been updated by nested instrumentation diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index d665abf95c86..a4a2b475e265 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -161,8 +161,10 @@ describe('instrumentDurableObjectWithSentry', () => { const after = flush.mock.calls.length; const delta = after - before; - // Verify that exactly one flush call was made during this test - expect(delta).toBe(1); + // Verify that two flush calls were made during this test + // The first flush is called when the response is captured + // The second flush is called when the waitUntil promises are finished + expect(delta).toBe(2); }); describe('instrumentPrototypeMethods option', () => { diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 15fa3effcd7f..b71088abbadf 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -161,7 +161,7 @@ describe('withSentry', () => { expect(waitUntil).toBeCalled(); vi.advanceTimersToNextTimer().runAllTimers(); await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); + expect(flush).toHaveBeenCalledTimes(2); }); });