Skip to content
Merged
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,51 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
TEST_DURABLE_OBJECT: DurableObjectNamespace;
}

class SyncKvDurableObjectBase extends DurableObject<Env> {
public constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}

async fetch(): Promise<Response> {
this.ctx.storage.kv.put('test-key', { value: 'hello' });
const val = this.ctx.storage.kv.get('test-key');
const entries = [...this.ctx.storage.kv.list()];
const deleted = this.ctx.storage.kv.delete('test-key');

return Response.json({ get: val, listSize: entries.length, deleted });
}
}

export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
SyncKvDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);

if (url.pathname === '/flush-marker') {
Sentry.captureMessage('flush-marker');
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
}

const id = env.TEST_DURABLE_OBJECT.idFromName('test');
const stub = env.TEST_DURABLE_OBJECT.get(id);
return stub.fetch(request);
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Envelope } from '@sentry/core';
import { expect, it } from 'vitest';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { createRunner } from '../../../runner';

const flushMarkerMatcher = (envelope: Envelope): void => {
const [, items] = envelope;
const [itemHeader, itemBody] = items[0] as [{ type: string }, Record<string, unknown>];

expect(itemHeader.type).toBe('event');
expect(itemBody.message).toBe('flush-marker');
};

it('instruments sync KV operations on Durable Object storage', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
const spans = transactionEvent?.spans ?? [];

expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'GET /',
}),
);

expect(spans).toHaveLength(4);
expect(spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'durable_object_storage_kv_put',
op: 'db',
origin: 'auto.db.cloudflare.durable_object',
data: expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object',
'db.system.name': 'cloudflare-durable-object-sql',
'db.operation.name': 'put',
}),
}),
expect.objectContaining({
description: 'durable_object_storage_kv_get',
op: 'db',
origin: 'auto.db.cloudflare.durable_object',
data: expect.objectContaining({
'db.system.name': 'cloudflare-durable-object-sql',
'db.operation.name': 'get',
}),
}),
expect.objectContaining({
description: 'durable_object_storage_kv_list',
op: 'db',
origin: 'auto.db.cloudflare.durable_object',
data: expect.objectContaining({
'db.system.name': 'cloudflare-durable-object-sql',
'db.operation.name': 'list',
}),
}),
expect.objectContaining({
description: 'durable_object_storage_kv_delete',
op: 'db',
origin: 'auto.db.cloudflare.durable_object',
data: expect.objectContaining({
'db.system.name': 'cloudflare-durable-object-sql',
'db.operation.name': 'delete',
}),
}),
]),
);
})
.expect(flushMarkerMatcher)
.start(signal);

await runner.makeRequest('get', '/');
await runner.makeRequest('get', '/flush-marker');
await runner.completed();
Comment thread
cursor[bot] marked this conversation as resolved.
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "worker-name",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"migrations": [
{
"new_sqlite_classes": ["TestDurableObject"],
"tag": "v1",
},
],
"durable_objects": {
"bindings": [
{
"class_name": "TestDurableObject",
"name": "TEST_DURABLE_OBJECT",
},
],
},
"compatibility_flags": ["nodejs_als"],
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DurableObjectStorage } from '@cloudflare/workers-types';
import type { DurableObjectStorage, SyncKvStorage } from '@cloudflare/workers-types';
import { isThenable, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { storeSpanContext } from '../utils/traceLinks';
import { instrumentDurableObjectSyncKvStorage } from './instrumentDurableObjectSyncKvStorage';

const STORAGE_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list', 'setAlarm', 'getAlarm', 'deleteAlarm'] as const;

Expand Down Expand Up @@ -35,6 +36,10 @@ export function instrumentDurableObjectStorage(
// reference" errors.
const original = Reflect.get(target, prop, target);

if (prop === 'kv' && original != null && 'get' in original && 'put' in original) {
return instrumentDurableObjectSyncKvStorage(original as SyncKvStorage);
}

if (typeof original !== 'function') {
return original;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { SyncKvStorage } from '@cloudflare/workers-types';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';

const SYNC_KV_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list'] as const;

type SyncKvMethod = (typeof SYNC_KV_METHODS_TO_INSTRUMENT)[number];

export function instrumentDurableObjectSyncKvStorage(syncKv: SyncKvStorage): SyncKvStorage {
return new Proxy(syncKv, {
get(target, prop, _receiver) {
const original = Reflect.get(target, prop, target);

if (typeof original !== 'function') {
return original;
}

const methodName = prop as SyncKvMethod;

if (!SYNC_KV_METHODS_TO_INSTRUMENT.includes(methodName)) {
return (original as (...args: unknown[]) => unknown).bind(target);
}

return function (this: unknown, ...args: unknown[]) {
return startSpan(
{
name: `durable_object_storage_kv_${methodName}`,
op: 'db',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object',
// keeping the value as close as possible to the Cloudflare Worker KV instrumentation
// https://github.com/cloudflare/workerd/blob/6b8b11787e2b2a800ab0edd0690bfab3857b0529/src/workerd/api/sync-kv.c%2B%2B#L19
'db.system.name': 'cloudflare-durable-object-sql',
Comment thread
JPeer264 marked this conversation as resolved.
'db.operation.name': methodName,
},
},
() => {
return (original as (...args: unknown[]) => unknown).apply(target, args);
},
);
};
},
});
}
29 changes: 29 additions & 0 deletions packages/cloudflare/test/instrumentDurableObjectStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import * as sentryCore from '@sentry/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { instrumentDurableObjectStorage } from '../src/instrumentations/instrumentDurableObjectStorage';
Expand Down Expand Up @@ -304,6 +305,28 @@ describe('instrumentDurableObjectStorage', () => {
});
});

describe('sync KV instrumentation', () => {
it('instruments the kv property with a proxy', () => {
const mockStorage = createMockStorage();
const instrumented = instrumentDurableObjectStorage(mockStorage);

instrumented.kv.get('myKey');

expect(sentryCore.startSpan).toHaveBeenCalledWith(
{
name: 'durable_object_storage_kv_get',
op: 'db',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object',
'db.system.name': 'cloudflare-durable-object-sql',
'db.operation.name': 'get',
},
},
expect.any(Function),
);
});
});

describe('native getter preservation', () => {
it('preserves native getter `this` binding through the proxy', () => {
// Private fields simulate workerd's native brand check —
Expand Down Expand Up @@ -349,5 +372,11 @@ function createMockStorage(): any {
sql: {
exec: vi.fn(),
},
kv: {
get: vi.fn().mockReturnValue(undefined),
put: vi.fn().mockReturnValue(undefined),
delete: vi.fn().mockReturnValue(false),
list: vi.fn().mockReturnValue([]),
},
};
}
Loading
Loading