Skip to content

Commit 462362f

Browse files
JPeer264claude
andauthored
test(e2e): Port cloudflare-workers to span streaming and keep a static copy (#23855)
Ports `cloudflare-workers` to span streaming and keeps the static trace lifecycle covered, via two renames: - `cloudflare-workers` → `cloudflare-workers-static`: verbatim, stays on `traceLifecycle: 'static'` with the transaction-based specs. Only the app name changes (`package.json` `name`, `proxyServerName` in `start-event-proxy.mjs`, the proxy name in every `waitFor*` call in `tests/`, and the wrangler worker `name`). - `cloudflare-workers-streaming` → `cloudflare-workers`: same name changes, plus the explicit `traceLifecycle: 'stream'` pin is dropped so the app exercises the default lifecycle. Its basic-request test now also pins the segment name: the root path `/` is a `route` source, so the span keeps the full `GET /` name under streaming. Renaming instead of copying keeps both apps' history followable and keeps the diff down to the name changes. Because the path `cloudflare-workers` exists before and after, git pairs the rename as `-streaming → -static` and shows `cloudflare-workers` as modified; the file contents follow the two moves above. Side effect of the rename: `cloudflare-workers` inherits the `-streaming` app's active memory test (`memory.test.ts`, already running in CI for that app); the two skipped memory tests stay with `cloudflare-workers-static`. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 606498f commit 462362f

16 files changed

Lines changed: 167 additions & 170 deletions

File tree

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/.gitignore renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/.gitignore

File renamed without changes.

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/package.json renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "cloudflare-workers-streaming",
2+
"name": "cloudflare-workers-static",
33
"version": "0.0.0",
44
"private": true,
55
"scripts": {

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/playwright.config.ts renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/playwright.config.ts

File renamed without changes.

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/env.d.ts renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/env.d.ts

File renamed without changes.

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ class MyDurableObjectBase extends DurableObject<Env> {
7676

7777
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
7878
(env: Env) => ({
79+
traceLifecycle: 'static',
7980
dsn: env.E2E_TEST_DSN,
8081
environment: 'qa', // dynamic sampling bias to keep transactions
8182
tunnel: `http://localhost:3031/`, // proxy server
8283
tracesSampleRate: 1.0,
83-
traceLifecycle: 'stream',
8484
transportOptions: {
8585
// We are doing a lot of events at once in this test
8686
bufferSize: 1000,
@@ -91,11 +91,11 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
9191

9292
export default Sentry.withSentry(
9393
(env: Env) => ({
94+
traceLifecycle: 'static',
9495
dsn: env.E2E_TEST_DSN,
9596
environment: 'qa', // dynamic sampling bias to keep transactions
9697
tunnel: `http://localhost:3031/`, // proxy server
9798
tracesSampleRate: 1.0,
98-
traceLifecycle: 'stream',
9999
transportOptions: {
100100
// We are doing a lot of events at once in this test
101101
bufferSize: 1000,

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/start-event-proxy.mjs renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/start-event-proxy.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
22

33
startEventProxyServer({
44
port: 3031,
5-
proxyServerName: 'cloudflare-workers-streaming',
5+
proxyServerName: 'cloudflare-workers-static',
66
});

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/index.test.ts renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import {
3-
getSpanOp,
4-
waitForError,
5-
waitForRequest,
6-
waitForStreamedSpan,
7-
waitForStreamedSpans,
8-
} from '@sentry-internal/test-utils';
2+
import { waitForError, waitForRequest, waitForTransaction } from '@sentry-internal/test-utils';
93
import { SDK_VERSION } from '@sentry/cloudflare';
104
import { WebSocket } from 'ws';
115

@@ -15,21 +9,8 @@ test('Index page', async ({ baseURL }) => {
159
await expect(result.text()).resolves.toBe('Hello World!');
1610
});
1711

18-
test('Sends a streamed span for a basic request', async ({ baseURL }) => {
19-
const spanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => {
20-
return getSpanOp(span) === 'http.server' && span.is_segment;
21-
});
22-
23-
await fetch(baseURL!);
24-
25-
const span = await spanPromise;
26-
27-
expect(span.trace_id).toMatch(/[a-f0-9]{32}/);
28-
expect(span.status).toBe('ok');
29-
});
30-
3112
test("worker's withSentry", async ({ baseURL }) => {
32-
const eventWaiter = waitForError('cloudflare-workers-streaming', event => {
13+
const eventWaiter = waitForError('cloudflare-workers-static', event => {
3314
return event.exception?.values?.[0]?.mechanism?.type === 'auto.http.cloudflare';
3415
});
3516
const response = await fetch(`${baseURL}/throwException`);
@@ -39,7 +20,7 @@ test("worker's withSentry", async ({ baseURL }) => {
3920
});
4021

4122
test('RPC method which throws an exception to be logged to sentry', async ({ baseURL }) => {
42-
const eventWaiter = waitForError('cloudflare-workers-streaming', event => {
23+
const eventWaiter = waitForError('cloudflare-workers-static', event => {
4324
return event.exception?.values?.[0]?.mechanism?.type === 'auto.faas.cloudflare.durable_object';
4425
});
4526
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
4930
});
5031

5132
test("Request processed by DurableObject's fetch is recorded", async ({ baseURL }) => {
52-
const eventWaiter = waitForError('cloudflare-workers-streaming', event => {
33+
const eventWaiter = waitForError('cloudflare-workers-static', event => {
5334
return event.exception?.values?.[0]?.mechanism?.type === 'auto.faas.cloudflare.durable_object';
5435
});
5536
const response = await fetch(`${baseURL}/pass-to-object/throwException`);
@@ -59,7 +40,7 @@ test("Request processed by DurableObject's fetch is recorded", async ({ baseURL
5940
});
6041

6142
test('Websocket.webSocketMessage', async ({ baseURL }) => {
62-
const eventWaiter = waitForError('cloudflare-workers-streaming', event => {
43+
const eventWaiter = waitForError('cloudflare-workers-static', event => {
6344
return !!event.exception?.values?.[0];
6445
});
6546
const url = new URL('/pass-to-object/ws', baseURL);
@@ -75,7 +56,7 @@ test('Websocket.webSocketMessage', async ({ baseURL }) => {
7556
});
7657

7758
test('Websocket.webSocketClose', async ({ baseURL }) => {
78-
const eventWaiter = waitForError('cloudflare-workers-streaming', event => {
59+
const eventWaiter = waitForError('cloudflare-workers-static', event => {
7960
return !!event.exception?.values?.[0];
8061
});
8162
const url = new URL('/pass-to-object/ws', baseURL);
@@ -91,38 +72,37 @@ test('Websocket.webSocketClose', async ({ baseURL }) => {
9172
});
9273

9374
test('sends user-agent header with SDK name and version in envelope requests', async ({ baseURL }) => {
94-
const requestPromise = waitForRequest('cloudflare-workers-streaming', () => true);
75+
const requestPromise = waitForRequest('cloudflare-workers-static', () => true);
9576

9677
await fetch(`${baseURL}/throwException`);
9778

9879
const request = await requestPromise;
9980

100-
expect(request.rawProxyRequestHeaders).toMatchObject({
101-
'user-agent': `sentry.javascript.cloudflare/${SDK_VERSION}`,
102-
});
81+
expect(request.rawProxyRequestHeaders['user-agent']).toBe(`sentry.javascript.cloudflare/${SDK_VERSION}`);
10382
});
10483

105-
test('Storage operations create spans in Durable Object', async ({ baseURL }) => {
106-
const spansPromise = waitForStreamedSpans('cloudflare-workers-streaming', spans => {
107-
return spans.some(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db');
84+
test('Storage operations create spans in Durable Object transactions', async ({ baseURL }) => {
85+
const transactionWaiter = waitForTransaction('cloudflare-workers-static', event => {
86+
return event.spans?.some(span => span.op === 'db' && span.description === 'durable_object_storage_put') ?? false;
10887
});
10988

11089
const response = await fetch(`${baseURL}/pass-to-object/storage/put`);
11190
expect(response.status).toBe(200);
11291

113-
const spans = await spansPromise;
114-
const putSpan = spans.find(span => span.name === 'durable_object_storage_put' && getSpanOp(span) === 'db');
92+
const transaction = await transactionWaiter;
93+
const putSpan = transaction.spans?.find(span => span.description === 'durable_object_storage_put');
11594

11695
expect(putSpan).toBeDefined();
117-
expect(putSpan?.attributes['db.system.name']?.value).toBe('cloudflare.durable_object.storage');
118-
expect(putSpan?.attributes['db.operation.name']?.value).toBe('put');
96+
expect(putSpan?.op).toBe('db');
97+
expect(putSpan?.data?.['db.system.name']).toBe('cloudflare.durable_object.storage');
98+
expect(putSpan?.data?.['db.operation.name']).toBe('put');
11999
});
120100

121101
test.describe('Alarm instrumentation', () => {
122102
test.describe.configure({ mode: 'serial' });
123103

124104
test('captures error from alarm handler', async ({ baseURL }) => {
125-
const errorWaiter = waitForError('cloudflare-workers-streaming', event => {
105+
const errorWaiter = waitForError('cloudflare-workers-static', event => {
126106
return event.exception?.values?.[0]?.value === 'Alarm error captured by Sentry';
127107
});
128108

@@ -133,31 +113,31 @@ test.describe('Alarm instrumentation', () => {
133113
expect(event.exception?.values?.[0]?.mechanism?.type).toBe('auto.faas.cloudflare.durable_object');
134114
});
135115

136-
test('creates a streamed span for alarm with new trace linked to setAlarm', async ({ baseURL }) => {
137-
const setAlarmSpanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => {
138-
return span.name === 'durable_object_storage_setAlarm' && span.is_segment === false;
116+
test('creates a transaction for alarm with new trace linked to setAlarm', async ({ baseURL }) => {
117+
const setAlarmTransactionWaiter = waitForTransaction('cloudflare-workers-static', event => {
118+
return event.spans?.some(span => span.description?.includes('storage_setAlarm')) ?? false;
139119
});
140120

141-
const alarmSpanPromise = waitForStreamedSpan('cloudflare-workers-streaming', span => {
142-
return span.name === 'alarm' && getSpanOp(span) === 'function' && span.is_segment;
121+
const alarmTransactionWaiter = waitForTransaction('cloudflare-workers-static', event => {
122+
return event.transaction === 'alarm' && event.contexts?.trace?.op === 'function';
143123
});
144124

145125
const response = await fetch(`${baseURL}/pass-to-object/setAlarm`);
146126
expect(response.status).toBe(200);
147127

148-
const setAlarmSpan = await setAlarmSpanPromise;
149-
const alarmSpan = await alarmSpanPromise;
128+
const setAlarmTransaction = await setAlarmTransactionWaiter;
129+
const alarmTransaction = await alarmTransactionWaiter;
150130

151-
// Alarm creates a streamed span with correct attributes
152-
expect(getSpanOp(alarmSpan)).toBe('function');
153-
expect(alarmSpan.attributes['sentry.origin']?.value).toBe('auto.faas.cloudflare.durable_object');
131+
// Alarm creates a transaction with correct attributes
132+
expect(alarmTransaction.contexts?.trace?.op).toBe('function');
133+
expect(alarmTransaction.contexts?.trace?.origin).toBe('auto.faas.cloudflare.durable_object');
154134

155135
// Alarm starts a new trace (different trace ID from the request that called setAlarm)
156-
expect(alarmSpan.trace_id).not.toBe(setAlarmSpan.trace_id);
136+
expect(alarmTransaction.contexts?.trace?.trace_id).not.toBe(setAlarmTransaction.contexts?.trace?.trace_id);
157137

158138
// Alarm links to the trace that called setAlarm via sentry.previous_trace attribute
159-
const previousTrace = alarmSpan.attributes['sentry.previous_trace']?.value;
139+
const previousTrace = alarmTransaction.contexts?.trace?.data?.['sentry.previous_trace'];
160140
expect(previousTrace).toBeDefined();
161-
expect(previousTrace).toContain(setAlarmSpan.trace_id);
141+
expect(previousTrace).toContain(setAlarmTransaction.contexts?.trace?.trace_id);
162142
});
163143
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { MemoryProfiler } from '@sentry-internal/test-utils';
2+
import { expect, test } from '@playwright/test';
3+
import { INSPECTOR_PORT } from '../playwright.config';
4+
5+
test.describe('Worker V8 isolate memory tests', () => {
6+
test.skip('worker memory is stable across request batches', async ({ baseURL }) => {
7+
const profiler = new MemoryProfiler({ port: INSPECTOR_PORT });
8+
9+
// Warm up: make initial requests and let the runtime settle
10+
for (let i = 0; i < 20; i++) {
11+
await fetch(baseURL!);
12+
}
13+
14+
await profiler.connect();
15+
16+
// First batch
17+
for (let i = 0; i < 50; i++) {
18+
const res = await fetch(baseURL!);
19+
expect(res.status).toBe(200);
20+
await res.text();
21+
}
22+
23+
const afterFirstBatch = await profiler.takeHeapSnapshot();
24+
25+
// Second batch
26+
for (let i = 0; i < 50; i++) {
27+
const res = await fetch(baseURL!);
28+
expect(res.status).toBe(200);
29+
await res.text();
30+
}
31+
32+
const afterSecondBatch = await profiler.takeHeapSnapshot();
33+
34+
// Compare batches to detect per-request leaks (excludes warm-up effects)
35+
const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch);
36+
37+
expect(result.nodeGrowthPercent).toBeLessThan(0.27);
38+
39+
await profiler.close();
40+
});
41+
42+
test.skip('durable object memory is stable across request batches', async ({ baseURL }) => {
43+
const profiler = new MemoryProfiler({ port: INSPECTOR_PORT });
44+
45+
// Warm up: let JIT compile, caches fill, and DO instance stabilize
46+
for (let i = 0; i < 30; i++) {
47+
await fetch(`${baseURL}/pass-to-object/storage/put`);
48+
}
49+
50+
await profiler.connect();
51+
52+
// First batch of requests to the same DO
53+
for (let i = 0; i < 50; i++) {
54+
const res = await fetch(`${baseURL}/pass-to-object/storage/put`);
55+
expect(res.status).toBe(200);
56+
await res.text();
57+
}
58+
59+
const afterFirstBatch = await profiler.takeHeapSnapshot();
60+
61+
// Second batch of requests to the same DO
62+
for (let i = 0; i < 50; i++) {
63+
const res = await fetch(`${baseURL}/pass-to-object/storage/put`);
64+
expect(res.status).toBe(200);
65+
await res.text();
66+
}
67+
68+
const afterSecondBatch = await profiler.takeHeapSnapshot();
69+
70+
// Compare batches to detect per-request leaks (excludes warm-up effects)
71+
// Before fix: makeFlushLock re-wrapped waitUntil on each request = leak
72+
// After fix: growth should be minimal
73+
const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch);
74+
75+
expect(result.nodeGrowthPercent).toBeLessThan(0.55);
76+
77+
await profiler.close();
78+
});
79+
});

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tests/tsconfig.json renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"extends": "../tsconfig.json",
3-
"compilerOptions": {
4-
"types": ["@cloudflare/vitest-pool-workers"]
5-
},
63
"include": ["./**/*.ts"],
74
"exclude": []
85
}

dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/tsconfig.json renamed to dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tsconfig.json

File renamed without changes.

0 commit comments

Comments
 (0)