diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/.gitignore rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/package.json similarity index 96% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/package.json rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/package.json index 88d31aae4b09..50ee6ec1c88d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/package.json @@ -1,5 +1,5 @@ { - "name": "cloudflare-workers-streaming", + "name": "cloudflare-workers-static", "version": "0.0.0", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/playwright.config.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/playwright.config.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/playwright.config.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/env.d.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/env.d.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/env.d.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/index.ts similarity index 98% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/index.ts index f59df9e8b33e..0887283330e8 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/index.ts @@ -76,11 +76,11 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ + traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, - traceLifecycle: 'stream', transportOptions: { // We are doing a lot of events at once in this test bufferSize: 1000, @@ -91,11 +91,11 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( export default Sentry.withSentry( (env: Env) => ({ + traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, - traceLifecycle: 'stream', transportOptions: { // We are doing a lot of events at once in this test bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/start-event-proxy.mjs similarity index 68% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/start-event-proxy.mjs index 58f1fbfb123c..d3c723c620f0 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'cloudflare-workers-streaming', + proxyServerName: 'cloudflare-workers-static', }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts similarity index 59% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/index.test.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts index e20d8e3823fb..8d53746ee43d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts @@ -1,11 +1,5 @@ import { expect, test } from '@playwright/test'; -import { - getSpanOp, - waitForError, - waitForRequest, - waitForStreamedSpan, - waitForStreamedSpans, -} from '@sentry-internal/test-utils'; +import { waitForError, waitForRequest, waitForTransaction } from '@sentry-internal/test-utils'; import { SDK_VERSION } from '@sentry/cloudflare'; import { WebSocket } from 'ws'; @@ -15,21 +9,8 @@ test('Index page', async ({ baseURL }) => { await expect(result.text()).resolves.toBe('Hello World!'); }); -test('Sends a streamed span for a basic request', async ({ baseURL }) => { - const spanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => { - return getSpanOp(span) === 'http.server' && span.is_segment; - }); - - await fetch(baseURL!); - - const span = await spanPromise; - - expect(span.trace_id).toMatch(/[a-f0-9]{32}/); - expect(span.status).toBe('ok'); -}); - test("worker's withSentry", async ({ baseURL }) => { - const eventWaiter = waitForError('cloudflare-workers-streaming', event => { + const eventWaiter = waitForError('cloudflare-workers-static', event => { return event.exception?.values?.[0]?.mechanism?.type === 'auto.http.cloudflare'; }); const response = await fetch(`${baseURL}/throwException`); @@ -39,7 +20,7 @@ test("worker's withSentry", async ({ baseURL }) => { }); test('RPC method which throws an exception to be logged to sentry', async ({ baseURL }) => { - const eventWaiter = waitForError('cloudflare-workers-streaming', event => { + const eventWaiter = waitForError('cloudflare-workers-static', event => { return event.exception?.values?.[0]?.mechanism?.type === 'auto.faas.cloudflare.durable_object'; }); const response = await fetch(`${baseURL}/rpc/throwException`); @@ -49,7 +30,7 @@ test('RPC method which throws an exception to be logged to sentry', async ({ bas }); test("Request processed by DurableObject's fetch is recorded", async ({ baseURL }) => { - const eventWaiter = waitForError('cloudflare-workers-streaming', event => { + const eventWaiter = waitForError('cloudflare-workers-static', event => { return event.exception?.values?.[0]?.mechanism?.type === 'auto.faas.cloudflare.durable_object'; }); const response = await fetch(`${baseURL}/pass-to-object/throwException`); @@ -59,7 +40,7 @@ test("Request processed by DurableObject's fetch is recorded", async ({ baseURL }); test('Websocket.webSocketMessage', async ({ baseURL }) => { - const eventWaiter = waitForError('cloudflare-workers-streaming', event => { + const eventWaiter = waitForError('cloudflare-workers-static', event => { return !!event.exception?.values?.[0]; }); const url = new URL('/pass-to-object/ws', baseURL); @@ -75,7 +56,7 @@ test('Websocket.webSocketMessage', async ({ baseURL }) => { }); test('Websocket.webSocketClose', async ({ baseURL }) => { - const eventWaiter = waitForError('cloudflare-workers-streaming', event => { + const eventWaiter = waitForError('cloudflare-workers-static', event => { return !!event.exception?.values?.[0]; }); const url = new URL('/pass-to-object/ws', baseURL); @@ -91,38 +72,37 @@ test('Websocket.webSocketClose', async ({ baseURL }) => { }); test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => { - const requestPromise = waitForRequest('cloudflare-workers-streaming', () => true); + const requestPromise = waitForRequest('cloudflare-workers-static', () => true); await fetch(`${baseURL}/throwException`); const request = await requestPromise; - expect(request.rawProxyRequestHeaders).toMatchObject({ - 'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`, - }); + expect(request.rawProxyRequestHeaders['user-agent']).toBe(`sentry.javascript.cloudflare/${SDK_VERSION}`); }); -test('Storage operations create spans in Durable Object', async ({ baseURL }) => { - const spansPromise = waitForStreamedSpans('cloudflare-workers-streaming', spans => { - return spans.some(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db'); +test('Storage operations create spans in Durable Object transactions', async ({ baseURL }) => { + const transactionWaiter = waitForTransaction('cloudflare-workers-static', event => { + return event.spans?.some(span => span.op === 'db' && span.description === 'durable_object_storage_put') ?? false; }); const response = await fetch(`${baseURL}/pass-to-object/storage/put`); expect(response.status).toBe(200); - const spans = await spansPromise; - const putSpan = spans.find(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db'); + const transaction = await transactionWaiter; + const putSpan = transaction.spans?.find(span => span.description === 'durable_object_storage_put'); expect(putSpan).toBeDefined(); - expect(putSpan?.attributes['db.system.name']?.value).toBe('cloudflare.durable_object.storage'); - expect(putSpan?.attributes['db.operation.name']?.value).toBe('put'); + expect(putSpan?.op).toBe('db'); + expect(putSpan?.data?.['db.system.name']).toBe('cloudflare.durable_object.storage'); + expect(putSpan?.data?.['db.operation.name']).toBe('put'); }); test.describe('Alarm instrumentation', () => { test.describe.configure({ mode: 'serial' }); test('captures error from alarm handler', async ({ baseURL }) => { - const errorWaiter = waitForError('cloudflare-workers-streaming', event => { + const errorWaiter = waitForError('cloudflare-workers-static', event => { return event.exception?.values?.[0]?.value === 'Alarm error captured by Sentry'; }); @@ -133,31 +113,31 @@ test.describe('Alarm instrumentation', () => { expect(event.exception?.values?.[0]?.mechanism?.type).toBe('auto.faas.cloudflare.durable_object'); }); - test('creates a streamed span for alarm with new trace linked to setAlarm', async ({ baseURL }) => { - const setAlarmSpanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => { - return span.name === 'durable_object_storage_setAlarm' && span.is_segment === false; + test('creates a transaction for alarm with new trace linked to setAlarm', async ({ baseURL }) => { + const setAlarmTransactionWaiter = waitForTransaction('cloudflare-workers-static', event => { + return event.spans?.some(span => span.description?.includes('storage_setAlarm')) ?? false; }); - const alarmSpanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => { - return span.name === 'alarm' && getSpanOp(span) === 'function' && span.is_segment; + const alarmTransactionWaiter = waitForTransaction('cloudflare-workers-static', event => { + return event.transaction === 'alarm' && event.contexts?.trace?.op === 'function'; }); const response = await fetch(`${baseURL}/pass-to-object/setAlarm`); expect(response.status).toBe(200); - const setAlarmSpan = await setAlarmSpanPromise; - const alarmSpan = await alarmSpanPromise; + const setAlarmTransaction = await setAlarmTransactionWaiter; + const alarmTransaction = await alarmTransactionWaiter; - // Alarm creates a streamed span with correct attributes - expect(getSpanOp(alarmSpan)).toBe('function'); - expect(alarmSpan.attributes['sentry.origin']?.value).toBe('auto.faas.cloudflare.durable_object'); + // Alarm creates a transaction with correct attributes + expect(alarmTransaction.contexts?.trace?.op).toBe('function'); + expect(alarmTransaction.contexts?.trace?.origin).toBe('auto.faas.cloudflare.durable_object'); // Alarm starts a new trace (different trace ID from the request that called setAlarm) - expect(alarmSpan.trace_id).not.toBe(setAlarmSpan.trace_id); + expect(alarmTransaction.contexts?.trace?.trace_id).not.toBe(setAlarmTransaction.contexts?.trace?.trace_id); // Alarm links to the trace that called setAlarm via sentry.previous_trace attribute - const previousTrace = alarmSpan.attributes['sentry.previous_trace']?.value; + const previousTrace = alarmTransaction.contexts?.trace?.data?.['sentry.previous_trace']; expect(previousTrace).toBeDefined(); - expect(previousTrace).toContain(setAlarmSpan.trace_id); + expect(previousTrace).toContain(setAlarmTransaction.contexts?.trace?.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/memory.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/memory.test.ts new file mode 100644 index 000000000000..4136e2ba4675 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/memory.test.ts @@ -0,0 +1,79 @@ +import { MemoryProfiler } from '@sentry-internal/test-utils'; +import { expect, test } from '@playwright/test'; +import { INSPECTOR_PORT } from '../playwright.config'; + +test.describe('Worker V8 isolate memory tests', () => { + test.skip('worker memory is stable across request batches', async ({ baseURL }) => { + const profiler = new MemoryProfiler({ port: INSPECTOR_PORT }); + + // Warm up: make initial requests and let the runtime settle + for (let i = 0; i < 20; i++) { + await fetch(baseURL!); + } + + await profiler.connect(); + + // First batch + for (let i = 0; i < 50; i++) { + const res = await fetch(baseURL!); + expect(res.status).toBe(200); + await res.text(); + } + + const afterFirstBatch = await profiler.takeHeapSnapshot(); + + // Second batch + for (let i = 0; i < 50; i++) { + const res = await fetch(baseURL!); + expect(res.status).toBe(200); + await res.text(); + } + + const afterSecondBatch = await profiler.takeHeapSnapshot(); + + // Compare batches to detect per-request leaks (excludes warm-up effects) + const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch); + + expect(result.nodeGrowthPercent).toBeLessThan(0.27); + + await profiler.close(); + }); + + test.skip('durable object memory is stable across request batches', async ({ baseURL }) => { + const profiler = new MemoryProfiler({ port: INSPECTOR_PORT }); + + // Warm up: let JIT compile, caches fill, and DO instance stabilize + for (let i = 0; i < 30; i++) { + await fetch(`${baseURL}/pass-to-object/storage/put`); + } + + await profiler.connect(); + + // First batch of requests to the same DO + for (let i = 0; i < 50; i++) { + const res = await fetch(`${baseURL}/pass-to-object/storage/put`); + expect(res.status).toBe(200); + await res.text(); + } + + const afterFirstBatch = await profiler.takeHeapSnapshot(); + + // Second batch of requests to the same DO + for (let i = 0; i < 50; i++) { + const res = await fetch(`${baseURL}/pass-to-object/storage/put`); + expect(res.status).toBe(200); + await res.text(); + } + + const afterSecondBatch = await profiler.takeHeapSnapshot(); + + // Compare batches to detect per-request leaks (excludes warm-up effects) + // Before fix: makeFlushLock re-wrapped waitUntil on each request = leak + // After fix: growth should be minimal + const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch); + + expect(result.nodeGrowthPercent).toBeLessThan(0.55); + + await profiler.close(); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/tsconfig.json similarity index 51% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/tsconfig.json rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/tsconfig.json index 80bfbd97acc1..fff8bae6e36f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/tsconfig.json @@ -1,8 +1,5 @@ { "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["@cloudflare/vitest-pool-workers"] - }, "include": ["./**/*.ts"], "exclude": [] } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tsconfig.json rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/wrangler.toml similarity index 99% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/wrangler.toml index d86500477814..095231e275eb 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/wrangler.toml @@ -1,5 +1,5 @@ #:schema node_modules/wrangler/config-schema.json -name = "cloudflare-workers-streaming" +name = "cloudflare-workers-static" main = "src/index.ts" compatibility_date = "2024-07-25" compatibility_flags = ["nodejs_compat"] diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/memory.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/memory.test.ts deleted file mode 100644 index 740961b3083f..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/memory.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MemoryProfiler } from '@sentry-internal/test-utils'; -import { expect, test } from '@playwright/test'; -import { INSPECTOR_PORT } from '../playwright.config'; - -test.describe('Worker V8 isolate memory tests', () => { - test('worker memory is reclaimed after GC', async ({ baseURL }) => { - const profiler = new MemoryProfiler({ port: INSPECTOR_PORT }); - - // Warm up: make initial requests and let the runtime settle - for (let i = 0; i < 5; i++) { - await fetch(baseURL!); - } - - await profiler.connect(); - - const baselineSnapshot = await profiler.takeHeapSnapshot(); - - for (let i = 0; i < 50; i++) { - const res = await fetch(baseURL!); - expect(res.status).toBe(200); - await res.text(); - } - - const finalSnapshot = await profiler.takeHeapSnapshot(); - const result = profiler.compareSnapshots(baselineSnapshot, finalSnapshot); - - expect(result.nodeGrowthPercent).toBeLessThan(1); - - await profiler.close(); - }); -}); 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 0887283330e8..a40ee67374ff 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 @@ -76,7 +76,6 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server @@ -91,7 +90,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( export default Sentry.withSentry( (env: Env) => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy 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 d43cb21770a0..3a5e72530801 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,11 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForRequest, waitForTransaction } from '@sentry-internal/test-utils'; +import { + getSpanOp, + waitForError, + waitForRequest, + waitForStreamedSpan, + waitForStreamedSpans, +} from '@sentry-internal/test-utils'; import { SDK_VERSION } from '@sentry/cloudflare'; import { WebSocket } from 'ws'; @@ -9,6 +15,22 @@ test('Index page', async ({ baseURL }) => { await expect(result.text()).resolves.toBe('Hello World!'); }); +test('Sends a streamed span for a basic request', async ({ baseURL }) => { + const spanPromise = waitForStreamedSpan('cloudflare-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment; + }); + + await fetch(baseURL!); + + const span = await spanPromise; + + // The root path is a low-cardinality route, so the span keeps the full `GET /` name under streaming. + expect(span.name).toBe('GET /'); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); + expect(span.status).toBe('ok'); + expect(span.attributes['sentry.segment.name.source']?.value).toBe('route'); +}); + test("worker's withSentry", async ({ baseURL }) => { const eventWaiter = waitForError('cloudflare-workers', event => { return event.exception?.values?.[0]?.mechanism?.type === 'auto.http.cloudflare'; @@ -78,26 +100,23 @@ test('sends user-agent header with SDK name and version in envelope requests', a const request = await requestPromise; - expect(request.rawProxyRequestHeaders).toMatchObject({ - 'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`, - }); + expect(request.rawProxyRequestHeaders['user-agent']).toBe(`sentry.javascript.cloudflare/${SDK_VERSION}`); }); -test('Storage operations create spans in Durable Object transactions', async ({ baseURL }) => { - const transactionWaiter = waitForTransaction('cloudflare-workers', event => { - return event.spans?.some(span => span.op === 'db' && span.description === 'durable_object_storage_put') ?? false; +test('Storage operations create spans in Durable Object', async ({ baseURL }) => { + const spansPromise = waitForStreamedSpans('cloudflare-workers', spans => { + return spans.some(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db'); }); const response = await fetch(`${baseURL}/pass-to-object/storage/put`); expect(response.status).toBe(200); - const transaction = await transactionWaiter; - const putSpan = transaction.spans?.find(span => span.description === 'durable_object_storage_put'); + const spans = await spansPromise; + const putSpan = spans.find(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db'); expect(putSpan).toBeDefined(); - expect(putSpan?.op).toBe('db'); - expect(putSpan?.data?.['db.system.name']).toBe('cloudflare.durable_object.storage'); - expect(putSpan?.data?.['db.operation.name']).toBe('put'); + expect(putSpan?.attributes['db.system.name']?.value).toBe('cloudflare.durable_object.storage'); + expect(putSpan?.attributes['db.operation.name']?.value).toBe('put'); }); test.describe('Alarm instrumentation', () => { @@ -115,31 +134,31 @@ test.describe('Alarm instrumentation', () => { expect(event.exception?.values?.[0]?.mechanism?.type).toBe('auto.faas.cloudflare.durable_object'); }); - test('creates a transaction for alarm with new trace linked to setAlarm', async ({ baseURL }) => { - const setAlarmTransactionWaiter = waitForTransaction('cloudflare-workers', event => { - return event.spans?.some(span => span.description?.includes('storage_setAlarm')) ?? false; + test('creates a streamed span for alarm with new trace linked to setAlarm', async ({ baseURL }) => { + const setAlarmSpanPromise = waitForStreamedSpan('cloudflare-workers', span => { + return span.name === 'durable_object_storage_setAlarm' && span.is_segment === false; }); - const alarmTransactionWaiter = waitForTransaction('cloudflare-workers', event => { - return event.transaction === 'alarm' && event.contexts?.trace?.op === 'function'; + const alarmSpanPromise = waitForStreamedSpan('cloudflare-workers', span => { + return span.name === 'alarm' && getSpanOp(span) === 'function' && span.is_segment; }); const response = await fetch(`${baseURL}/pass-to-object/setAlarm`); expect(response.status).toBe(200); - const setAlarmTransaction = await setAlarmTransactionWaiter; - const alarmTransaction = await alarmTransactionWaiter; + const setAlarmSpan = await setAlarmSpanPromise; + const alarmSpan = await alarmSpanPromise; - // Alarm creates a transaction with correct attributes - expect(alarmTransaction.contexts?.trace?.op).toBe('function'); - expect(alarmTransaction.contexts?.trace?.origin).toBe('auto.faas.cloudflare.durable_object'); + // Alarm creates a streamed span with correct attributes + expect(getSpanOp(alarmSpan)).toBe('function'); + expect(alarmSpan.attributes['sentry.origin']?.value).toBe('auto.faas.cloudflare.durable_object'); // Alarm starts a new trace (different trace ID from the request that called setAlarm) - expect(alarmTransaction.contexts?.trace?.trace_id).not.toBe(setAlarmTransaction.contexts?.trace?.trace_id); + expect(alarmSpan.trace_id).not.toBe(setAlarmSpan.trace_id); // Alarm links to the trace that called setAlarm via sentry.previous_trace attribute - const previousTrace = alarmTransaction.contexts?.trace?.data?.['sentry.previous_trace']; + const previousTrace = alarmSpan.attributes['sentry.previous_trace']?.value; expect(previousTrace).toBeDefined(); - expect(previousTrace).toContain(setAlarmTransaction.contexts?.trace?.trace_id); + expect(previousTrace).toContain(setAlarmSpan.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts index 4136e2ba4675..740961b3083f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts @@ -3,76 +3,28 @@ import { expect, test } from '@playwright/test'; import { INSPECTOR_PORT } from '../playwright.config'; test.describe('Worker V8 isolate memory tests', () => { - test.skip('worker memory is stable across request batches', async ({ baseURL }) => { + test('worker memory is reclaimed after GC', async ({ baseURL }) => { const profiler = new MemoryProfiler({ port: INSPECTOR_PORT }); // Warm up: make initial requests and let the runtime settle - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 5; i++) { await fetch(baseURL!); } await profiler.connect(); - // First batch - for (let i = 0; i < 50; i++) { - const res = await fetch(baseURL!); - expect(res.status).toBe(200); - await res.text(); - } - - const afterFirstBatch = await profiler.takeHeapSnapshot(); + const baselineSnapshot = await profiler.takeHeapSnapshot(); - // Second batch for (let i = 0; i < 50; i++) { const res = await fetch(baseURL!); expect(res.status).toBe(200); await res.text(); } - const afterSecondBatch = await profiler.takeHeapSnapshot(); - - // Compare batches to detect per-request leaks (excludes warm-up effects) - const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch); - - expect(result.nodeGrowthPercent).toBeLessThan(0.27); - - await profiler.close(); - }); - - test.skip('durable object memory is stable across request batches', async ({ baseURL }) => { - const profiler = new MemoryProfiler({ port: INSPECTOR_PORT }); - - // Warm up: let JIT compile, caches fill, and DO instance stabilize - for (let i = 0; i < 30; i++) { - await fetch(`${baseURL}/pass-to-object/storage/put`); - } - - await profiler.connect(); - - // First batch of requests to the same DO - for (let i = 0; i < 50; i++) { - const res = await fetch(`${baseURL}/pass-to-object/storage/put`); - expect(res.status).toBe(200); - await res.text(); - } - - const afterFirstBatch = await profiler.takeHeapSnapshot(); - - // Second batch of requests to the same DO - for (let i = 0; i < 50; i++) { - const res = await fetch(`${baseURL}/pass-to-object/storage/put`); - expect(res.status).toBe(200); - await res.text(); - } - - const afterSecondBatch = await profiler.takeHeapSnapshot(); - - // Compare batches to detect per-request leaks (excludes warm-up effects) - // Before fix: makeFlushLock re-wrapped waitUntil on each request = leak - // After fix: growth should be minimal - const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch); + const finalSnapshot = await profiler.takeHeapSnapshot(); + const result = profiler.compareSnapshots(baselineSnapshot, finalSnapshot); - expect(result.nodeGrowthPercent).toBeLessThan(0.55); + expect(result.nodeGrowthPercent).toBeLessThan(1); await profiler.close(); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json index fff8bae6e36f..80bfbd97acc1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/vitest-pool-workers"] + }, "include": ["./**/*.ts"], "exclude": [] }