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
@@ -0,0 +1,32 @@
import * as Sentry from '@sentry/cloudflare';

interface Env {
SENTRY_DSN: string;
DB: D1Database;
}

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(request, env, _ctx) {
const url = new URL(request.url);
const db = Sentry.instrumentD1WithSentry(env.DB);

if (url.pathname === '/init') {
await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
await db.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();
return new Response('Initialized');
}

if (url.pathname === '/query') {
const result = await db.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first();
return Response.json(result);
}

return new Response('OK');
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, it } from 'vitest';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { createRunner } from '../../../runner';

it('D1 database queries create spans with correct attributes', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'GET /init',
spans: [
{
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
'cloudflare.d1.query_type': 'run',
'cloudflare.d1.duration': expect.any(Number),
'cloudflare.d1.rows_read': expect.any(Number),
'cloudflare.d1.rows_written': expect.any(Number),
},
description: 'INSERT INTO users (name) VALUES (?)',
op: 'db.query',
origin: 'auto.db.cloudflare.d1',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
},
],
}),
);
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'GET /query',
spans: [
{
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
'cloudflare.d1.query_type': 'first',
},
description: 'SELECT * FROM users WHERE name = ?',
op: 'db.query',
origin: 'auto.db.cloudflare.d1',
parent_span_id: expect.any(String),
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.any(String),
},
],
}),
);
})
.start(signal);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D1 test missing .unordered() risks flaky ordering

Low Severity

The D1 test expects two transaction envelopes (GET /init then GET /query) in strict order without calling .unordered(). Since the SDK flushes telemetry asynchronously via waitUntil, the second transaction could arrive at the mock server before the first, causing the ordered assertion to fail. Every other multi-envelope test in this project (hono-sdk, hono-integration, durableobject, workflow) uses .unordered() to avoid this. The two expectations are already distinguishable by transaction name, so .unordered() would be safe here.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 204c0ae. Configure here.


await runner.makeRequest('get', '/init');
await runner.makeRequest('get', '/query');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "d1-worker",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "test-db",
"database_id": "local-test-db",
},
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Sentry from '@sentry/cloudflare';

interface Env {
SENTRY_DSN: string;
}

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(_request, _env, _ctx) {
return new Response('OK');
},
async scheduled(_controller, _env, _ctx) {
// Successful scheduled handler - just does some work
await new Promise(resolve => setTimeout(resolve, 10));
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, it } from 'vitest';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
} from '@sentry/core';
import { createRunner } from '../../../runner';

it('Scheduled handler creates transaction with correct attributes', async ({ signal }) => {
const runner = createRunner(__dirname)
.withWranglerArgs('--test-scheduled')
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: expect.stringMatching(/^Scheduled Cron/),
transaction_info: { source: 'task' },
spans: [],
contexts: expect.objectContaining({
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
op: 'faas.cron',
origin: 'auto.faas.cloudflare.scheduled',
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'faas.cron',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
'faas.cron': expect.any(String),
'faas.time': expect.any(String),
'faas.trigger': 'timer',
},
},
}),
}),
);
})
.start(signal);

await runner.makeRequest('get', '/__scheduled');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "scheduled-worker",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
"triggers": {
"crons": ["* * * * *"],
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as Sentry from '@sentry/cloudflare';
import { WorkflowEntrypoint } from 'cloudflare:workers';
import type { WorkflowEvent, WorkflowStep } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_WORKFLOW: Workflow;
}

class MyWorkflowBase extends WorkflowEntrypoint<Env> {
async run(_event: WorkflowEvent<unknown>, step: WorkflowStep): Promise<void> {
await step.do('step-one', async () => {
return 'Step one completed';
});

await step.do('step-two', async () => {
return 'Step two completed';
});
}
}

export const MyWorkflow = Sentry.instrumentWorkflowWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
MyWorkflowBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/workflow/trigger') {
const instance = await env.MY_WORKFLOW.create();
for (let i = 0; i < 15; i++) {
try {
const s = await instance.status();
if (s.status === 'complete' || s.status === 'errored') {
return new Response(JSON.stringify({ id: instance.id, ...s }), {
headers: { 'content-type': 'application/json' },
});
}
} catch {
// status() may not be available in local dev
}
await new Promise(r => setTimeout(r, 500));
}
return new Response(JSON.stringify({ id: instance.id, status: 'timeout' }), {
headers: { 'content-type': 'application/json' },
});
}
return new Response('OK');
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, it } from 'vitest';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
} from '@sentry/core';
import { createRunner } from '../../../runner';

it('Workflow steps create transactions with correct attributes', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'step-one',
transaction_info: { source: 'task' },
spans: [],
contexts: expect.objectContaining({
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
op: 'function.step.do',
origin: 'auto.faas.cloudflare.workflow',
status: 'ok',
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.step.do',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
},
},
}),
}),
);
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'step-two',
transaction_info: { source: 'task' },
spans: [],
contexts: expect.objectContaining({
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
op: 'function.step.do',
origin: 'auto.faas.cloudflare.workflow',
status: 'ok',
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.step.do',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
},
},
}),
}),
);
})
.unordered()
.start(signal);

await runner.makeRequest('get', '/workflow/trigger');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "workflow-worker",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_compat"],
"workflows": [
{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow",
},
],
}
Loading