Skip to content

Commit 204c0ae

Browse files
committed
test(cloudflare): Add integration tests for scheduled, D1, and workflow
1 parent 124dfeb commit 204c0ae

9 files changed

Lines changed: 329 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
3+
interface Env {
4+
SENTRY_DSN: string;
5+
DB: D1Database;
6+
}
7+
8+
export default Sentry.withSentry(
9+
(env: Env) => ({
10+
dsn: env.SENTRY_DSN,
11+
tracesSampleRate: 1.0,
12+
}),
13+
{
14+
async fetch(request, env, _ctx) {
15+
const url = new URL(request.url);
16+
const db = Sentry.instrumentD1WithSentry(env.DB);
17+
18+
if (url.pathname === '/init') {
19+
await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
20+
await db.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();
21+
return new Response('Initialized');
22+
}
23+
24+
if (url.pathname === '/query') {
25+
const result = await db.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first();
26+
return Response.json(result);
27+
}
28+
29+
return new Response('OK');
30+
},
31+
} satisfies ExportedHandler<Env>,
32+
);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { expect, it } from 'vitest';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
3+
import { createRunner } from '../../../runner';
4+
5+
it('D1 database queries create spans with correct attributes', async ({ signal }) => {
6+
const runner = createRunner(__dirname)
7+
.expect(envelope => {
8+
const transactionEvent = envelope[1]?.[0]?.[1];
9+
expect(transactionEvent).toEqual(
10+
expect.objectContaining({
11+
type: 'transaction',
12+
transaction: 'GET /init',
13+
spans: [
14+
{
15+
data: {
16+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
17+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
18+
'cloudflare.d1.query_type': 'run',
19+
'cloudflare.d1.duration': expect.any(Number),
20+
'cloudflare.d1.rows_read': expect.any(Number),
21+
'cloudflare.d1.rows_written': expect.any(Number),
22+
},
23+
description: 'INSERT INTO users (name) VALUES (?)',
24+
op: 'db.query',
25+
origin: 'auto.db.cloudflare.d1',
26+
parent_span_id: expect.any(String),
27+
span_id: expect.any(String),
28+
start_timestamp: expect.any(Number),
29+
timestamp: expect.any(Number),
30+
trace_id: expect.any(String),
31+
},
32+
],
33+
}),
34+
);
35+
})
36+
.expect(envelope => {
37+
const transactionEvent = envelope[1]?.[0]?.[1];
38+
expect(transactionEvent).toEqual(
39+
expect.objectContaining({
40+
type: 'transaction',
41+
transaction: 'GET /query',
42+
spans: [
43+
{
44+
data: {
45+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
46+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
47+
'cloudflare.d1.query_type': 'first',
48+
},
49+
description: 'SELECT * FROM users WHERE name = ?',
50+
op: 'db.query',
51+
origin: 'auto.db.cloudflare.d1',
52+
parent_span_id: expect.any(String),
53+
span_id: expect.any(String),
54+
start_timestamp: expect.any(Number),
55+
timestamp: expect.any(Number),
56+
trace_id: expect.any(String),
57+
},
58+
],
59+
}),
60+
);
61+
})
62+
.start(signal);
63+
64+
await runner.makeRequest('get', '/init');
65+
await runner.makeRequest('get', '/query');
66+
await runner.completed();
67+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "d1-worker",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"],
6+
"d1_databases": [
7+
{
8+
"binding": "DB",
9+
"database_name": "test-db",
10+
"database_id": "local-test-db",
11+
},
12+
],
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
3+
interface Env {
4+
SENTRY_DSN: string;
5+
}
6+
7+
export default Sentry.withSentry(
8+
(env: Env) => ({
9+
dsn: env.SENTRY_DSN,
10+
tracesSampleRate: 1.0,
11+
}),
12+
{
13+
async fetch(_request, _env, _ctx) {
14+
return new Response('OK');
15+
},
16+
async scheduled(_controller, _env, _ctx) {
17+
// Successful scheduled handler - just does some work
18+
await new Promise(resolve => setTimeout(resolve, 10));
19+
},
20+
} satisfies ExportedHandler<Env>,
21+
);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, it } from 'vitest';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
5+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
7+
} from '@sentry/core';
8+
import { createRunner } from '../../../runner';
9+
10+
it('Scheduled handler creates transaction with correct attributes', async ({ signal }) => {
11+
const runner = createRunner(__dirname)
12+
.withWranglerArgs('--test-scheduled')
13+
.expect(envelope => {
14+
const transactionEvent = envelope[1]?.[0]?.[1];
15+
expect(transactionEvent).toEqual(
16+
expect.objectContaining({
17+
type: 'transaction',
18+
transaction: expect.stringMatching(/^Scheduled Cron/),
19+
transaction_info: { source: 'task' },
20+
spans: [],
21+
contexts: expect.objectContaining({
22+
trace: {
23+
span_id: expect.any(String),
24+
trace_id: expect.any(String),
25+
op: 'faas.cron',
26+
origin: 'auto.faas.cloudflare.scheduled',
27+
data: {
28+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'faas.cron',
29+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
30+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
31+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
32+
'faas.cron': expect.any(String),
33+
'faas.time': expect.any(String),
34+
'faas.trigger': 'timer',
35+
},
36+
},
37+
}),
38+
}),
39+
);
40+
})
41+
.start(signal);
42+
43+
await runner.makeRequest('get', '/__scheduled');
44+
await runner.completed();
45+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "scheduled-worker",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"],
6+
"triggers": {
7+
"crons": ["* * * * *"],
8+
},
9+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
import { WorkflowEntrypoint } from 'cloudflare:workers';
3+
import type { WorkflowEvent, WorkflowStep } from 'cloudflare:workers';
4+
5+
interface Env {
6+
SENTRY_DSN: string;
7+
MY_WORKFLOW: Workflow;
8+
}
9+
10+
class MyWorkflowBase extends WorkflowEntrypoint<Env> {
11+
async run(_event: WorkflowEvent<unknown>, step: WorkflowStep): Promise<void> {
12+
await step.do('step-one', async () => {
13+
return 'Step one completed';
14+
});
15+
16+
await step.do('step-two', async () => {
17+
return 'Step two completed';
18+
});
19+
}
20+
}
21+
22+
export const MyWorkflow = Sentry.instrumentWorkflowWithSentry(
23+
(env: Env) => ({
24+
dsn: env.SENTRY_DSN,
25+
tracesSampleRate: 1.0,
26+
}),
27+
MyWorkflowBase,
28+
);
29+
30+
export default Sentry.withSentry(
31+
(env: Env) => ({
32+
dsn: env.SENTRY_DSN,
33+
tracesSampleRate: 1.0,
34+
}),
35+
{
36+
async fetch(request, env) {
37+
const url = new URL(request.url);
38+
if (url.pathname === '/workflow/trigger') {
39+
const instance = await env.MY_WORKFLOW.create();
40+
for (let i = 0; i < 15; i++) {
41+
try {
42+
const s = await instance.status();
43+
if (s.status === 'complete' || s.status === 'errored') {
44+
return new Response(JSON.stringify({ id: instance.id, ...s }), {
45+
headers: { 'content-type': 'application/json' },
46+
});
47+
}
48+
} catch {
49+
// status() may not be available in local dev
50+
}
51+
await new Promise(r => setTimeout(r, 500));
52+
}
53+
return new Response(JSON.stringify({ id: instance.id, status: 'timeout' }), {
54+
headers: { 'content-type': 'application/json' },
55+
});
56+
}
57+
return new Response('OK');
58+
},
59+
} satisfies ExportedHandler<Env>,
60+
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { expect, it } from 'vitest';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
5+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
7+
} from '@sentry/core';
8+
import { createRunner } from '../../../runner';
9+
10+
it('Workflow steps create transactions with correct attributes', async ({ signal }) => {
11+
const runner = createRunner(__dirname)
12+
.expect(envelope => {
13+
const transactionEvent = envelope[1]?.[0]?.[1];
14+
expect(transactionEvent).toEqual(
15+
expect.objectContaining({
16+
type: 'transaction',
17+
transaction: 'step-one',
18+
transaction_info: { source: 'task' },
19+
spans: [],
20+
contexts: expect.objectContaining({
21+
trace: {
22+
span_id: expect.any(String),
23+
trace_id: expect.any(String),
24+
op: 'function.step.do',
25+
origin: 'auto.faas.cloudflare.workflow',
26+
status: 'ok',
27+
data: {
28+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.step.do',
29+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
30+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
31+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
32+
},
33+
},
34+
}),
35+
}),
36+
);
37+
})
38+
.expect(envelope => {
39+
const transactionEvent = envelope[1]?.[0]?.[1];
40+
expect(transactionEvent).toEqual(
41+
expect.objectContaining({
42+
type: 'transaction',
43+
transaction: 'step-two',
44+
transaction_info: { source: 'task' },
45+
spans: [],
46+
contexts: expect.objectContaining({
47+
trace: {
48+
span_id: expect.any(String),
49+
trace_id: expect.any(String),
50+
op: 'function.step.do',
51+
origin: 'auto.faas.cloudflare.workflow',
52+
status: 'ok',
53+
data: {
54+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.step.do',
55+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
56+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
57+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
58+
},
59+
},
60+
}),
61+
}),
62+
);
63+
})
64+
.unordered()
65+
.start(signal);
66+
67+
await runner.makeRequest('get', '/workflow/trigger');
68+
await runner.completed();
69+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "workflow-worker",
3+
"main": "index.ts",
4+
"compatibility_date": "2025-06-17",
5+
"compatibility_flags": ["nodejs_compat"],
6+
"workflows": [
7+
{
8+
"name": "my-workflow",
9+
"binding": "MY_WORKFLOW",
10+
"class_name": "MyWorkflow",
11+
},
12+
],
13+
}

0 commit comments

Comments
 (0)