Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "cloudflare-workers-streaming",
"name": "cloudflare-workers-static",
"version": "0.0.0",
"private": true,
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ class MyDurableObjectBase extends DurableObject<Env> {

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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'cloudflare-workers-streaming',
proxyServerName: 'cloudflare-workers-static',
});
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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`);
Expand All @@ -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`);
Expand All @@ -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`);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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';
});

Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts"],
"exclude": []
}
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ class MyDurableObjectBase extends DurableObject<Env> {

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
Expand All @@ -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
Expand Down
Loading
Loading