Skip to content
Draft
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,7 +1,14 @@
import type { DurableObjectStorage, SyncKvStorage, SqlStorage } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { DB } from '@sentry/conventions/op';
import { getClient, isThenable, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import {
getActiveSpan,
getClient,
isThenable,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanIsSampled,
startSpan,
} from '@sentry/core';
import type { CloudflareClientOptions } from '../client';
import { getStorageKeys, targetsCloudflareInternalKey } from '../utils/internalStorageKey';
import { storeSpanContext } from '../utils/traceLinks';
Expand Down Expand Up @@ -59,6 +66,12 @@ export function instrumentDurableObjectStorage(
}

return function (this: unknown, ...args: unknown[]) {
// `setAlarm` keeps its span, because the alarm links back to the span context stored here.
const activeSpan = getActiveSpan();
if (methodName !== 'setAlarm' && activeSpan && !spanIsSampled(activeSpan)) {
return (original as (...a: unknown[]) => unknown).apply(target, args);
}

// KV entries managed by the DO framework itself (agents/partyserver state) are bookkeeping
// rather than user work — skip the span, mirroring how `cf_` SQL tables are treated.
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- rule false positive: the cast reaches the Cloudflare-only `durableObjectStorageSpanAllowlist`; tsc errors without it
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SyncKvStorage } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { DB } from '@sentry/conventions/op';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanIsSampled, startSpan } from '@sentry/core';

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

Expand All @@ -23,6 +23,11 @@ export function instrumentDurableObjectSyncKvStorage(syncKv: SyncKvStorage): Syn
}

return function (this: unknown, ...args: unknown[]) {
const activeSpan = getActiveSpan();
if (activeSpan && !spanIsSampled(activeSpan)) {
return (original as (...args: unknown[]) => unknown).apply(target, args);
}

return startSpan(
{
name: `durable_object_storage_kv_${methodName}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SqlStorage } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { DB_QUERY } from '@sentry/conventions/op';
import { getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { getActiveSpan, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanIsSampled, startSpan } from '@sentry/core';
import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils';
import type { CloudflareClientOptions } from '../client';
import { targetsCloudflareInternalTable } from '../utils/internalSqlQuery';
Expand All @@ -24,6 +24,11 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage {
return function (this: unknown, ...args: unknown[]) {
const [query, ...bindings] = args as [string, ...unknown[]];

const activeSpan = getActiveSpan();
if (activeSpan && !spanIsSampled(activeSpan)) {
return (original as (...a: unknown[]) => ReturnType<SqlStorage['exec']>).apply(target, args);
}

const sanitizedQuery = sanitizeSqlQuery(query);
const querySummary = getSqlQuerySummary(sanitizedQuery);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MessageSendRequest, Queue, QueueSendBatchOptions, QueueSendOptions } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { QUEUE_PUBLISH } from '@sentry/conventions/op';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanIsSampled, startSpan } from '@sentry/core';

const ORIGIN = 'auto.faas.cloudflare.queue';

Expand Down Expand Up @@ -71,6 +71,11 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
const activeSpan = getActiveSpan();
if (activeSpan && !spanIsSampled(activeSpan)) {
return Reflect.apply(original, target, [message, options]);
}

return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All @@ -84,6 +89,11 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): ReturnType<Queue['sendBatch']> {
const activeSpan = getActiveSpan();
if (activeSpan && !spanIsSampled(activeSpan)) {
return Reflect.apply(original, target, [messages, options]);
}

const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
OBJECT_PUT,
OBJECT_UPLOAD_PART,
} from '@sentry/conventions/op';
import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { getActiveSpan, isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanIsSampled, startSpan } from '@sentry/core';

const ORIGIN = 'auto.faas.cloudflare.r2';

Expand Down Expand Up @@ -72,6 +72,11 @@ function createSpanOptions(bindingName: string, r2Op: R2OperationKey, key?: stri
};
}

function isUnsampled(): boolean {
const activeSpan = getActiveSpan();
return !!activeSpan && !spanIsSampled(activeSpan);
}

function instrumentR2MultipartUpload(upload: R2MultipartUpload, bindingName: string): R2MultipartUpload {
const { key } = upload;

Expand All @@ -81,6 +86,10 @@ function instrumentR2MultipartUpload(upload: R2MultipartUpload, bindingName: str
const original = Reflect.get(target, prop, receiver);

return function (this: unknown, ...args: Parameters<R2MultipartUpload['uploadPart']>) {
if (isUnsampled()) {
return Reflect.apply(original, target, args);
}

const [partNumber] = args;
const spanOptions = createSpanOptions(bindingName, 'uploadPart', key);

Expand All @@ -101,6 +110,10 @@ function instrumentR2MultipartUpload(upload: R2MultipartUpload, bindingName: str
const original = Reflect.get(target, prop, receiver);

return function (this: unknown) {
if (isUnsampled()) {
return Reflect.apply(original, target, []);
}

return startSpan(createSpanOptions(bindingName, 'abortMultipartUpload', key), () =>
Reflect.apply(original, target, []),
);
Expand All @@ -111,6 +124,10 @@ function instrumentR2MultipartUpload(upload: R2MultipartUpload, bindingName: str
const original = Reflect.get(target, prop, receiver);

return function (this: unknown, ...args: Parameters<R2MultipartUpload['complete']>) {
if (isUnsampled()) {
return Reflect.apply(original, target, args);
}

return startSpan(createSpanOptions(bindingName, 'completeMultipartUpload', key), () =>
Reflect.apply(original, target, args),
);
Expand All @@ -135,6 +152,10 @@ export function instrumentR2Bucket<T extends R2Bucket>(bucket: T, bindingName: s
const original = Reflect.get(target, prop, receiver);

return function (this: unknown, ...args: Parameters<R2Bucket[typeof prop]>) {
if (isUnsampled()) {
return Reflect.apply(original, target, args);
}

const [key] = args;

return startSpan(createSpanOptions(bindingName, prop, key), () => Reflect.apply(original, target, args));
Expand All @@ -145,6 +166,12 @@ export function instrumentR2Bucket<T extends R2Bucket>(bucket: T, bindingName: s
const original = Reflect.get(target, prop, receiver) as R2Bucket['createMultipartUpload'];

return function (this: unknown, ...args: Parameters<R2Bucket['createMultipartUpload']>) {
if (isUnsampled()) {
return Reflect.apply(original, target, args).then(upload =>
instrumentR2MultipartUpload(upload, bindingName),
);
}

const [key] = args;

return startSpan(createSpanOptions(bindingName, 'createMultipartUpload', key), async () => {
Expand Down
39 changes: 39 additions & 0 deletions packages/cloudflare/test/instrumentDurableObjectStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as sentryCore from '@sentry/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { instrumentDurableObjectStorage } from '../src/instrumentations/instrumentDurableObjectStorage';
import * as traceLinks from '../src/utils/traceLinks';
import { initTestClient, resetSdk } from './testUtils';

vi.mock('../src/utils/traceLinks', async importOriginal => {
const actual = await importOriginal<typeof traceLinks>();
Expand Down Expand Up @@ -474,6 +475,44 @@ describe('instrumentDurableObjectStorage', () => {
await expect(instrumented.get('myKey')).rejects.toThrow('Storage error');
});
});

describe('inside a parent span', () => {
afterEach(() => {
resetSdk();
});

it.each([
[1, true],
[0, false],
])('with tracesSampleRate %s, starts a span for KV methods: %s', (tracesSampleRate, startsSpan) => {
initTestClient({ tracesSampleRate });
const mockStorage = createMockStorage();
const instrumented = instrumentDurableObjectStorage(mockStorage);

sentryCore.startSpan({ name: 'parent' }, () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');

void instrumented.get('myKey');

expect(startSpanSpy).toHaveBeenCalledTimes(startsSpan ? 1 : 0);
});

expect(mockStorage.get).toHaveBeenCalledWith('myKey');
});

it('with tracesSampleRate 0, still starts a span for setAlarm', () => {
initTestClient({ tracesSampleRate: 0 });
const instrumented = instrumentDurableObjectStorage(createMockStorage());

sentryCore.startSpan({ name: 'parent' }, () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');

void instrumented.setAlarm(Date.now() + 1000);

expect(startSpanSpy).toHaveBeenCalledTimes(1);
});
});
});
});

function createMockStorage(): any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import * as sentryCore from '@sentry/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { instrumentDurableObjectSyncKvStorage } from '../src/instrumentations/instrumentDurableObjectSyncKvStorage';
import { initTestClient, resetSdk } from './testUtils';

describe('instrumentDurableObjectSyncKvStorage', () => {
afterEach(() => {
Expand Down Expand Up @@ -185,6 +186,31 @@ describe('instrumentDurableObjectSyncKvStorage', () => {
expect(() => instrumented.get('myKey')).toThrow('Storage error');
});
});

describe('inside a parent span', () => {
afterEach(() => {
resetSdk();
});

it.each([
[1, true],
[0, false],
])('with tracesSampleRate %s, starts a span: %s', (tracesSampleRate, startsSpan) => {
initTestClient({ tracesSampleRate });
const mockKv = createMockSyncKv();
const instrumented = instrumentDurableObjectSyncKvStorage(mockKv);

sentryCore.startSpan({ name: 'parent' }, () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');

instrumented.get('myKey');

expect(startSpanSpy).toHaveBeenCalledTimes(startsSpan ? 1 : 0);
});

expect(mockKv.get).toHaveBeenCalledWith('myKey');
});
});
});

function createMockSyncKv(): any {
Expand Down
29 changes: 29 additions & 0 deletions packages/cloudflare/test/instrumentSqlStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import * as sentryCore from '@sentry/core';
import * as serverUtils from '@sentry/server-utils';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { instrumentSqlStorage } from '../src/instrumentations/instrumentSqlStorage';
import { initTestClient, resetSdk } from './testUtils';

describe('instrumentSqlStorage', () => {
afterEach(() => {
Expand Down Expand Up @@ -259,6 +261,33 @@ describe('instrumentSqlStorage', () => {
});
});
});

describe('inside a parent span', () => {
afterEach(() => {
resetSdk();
});

it.each([
[1, true],
[0, false],
])('with tracesSampleRate %s, sanitizes the query and starts a span: %s', (tracesSampleRate, startsSpan) => {
initTestClient({ tracesSampleRate });
const sanitizeSpy = vi.spyOn(serverUtils, 'sanitizeSqlQuery');
const mockSql = createMockSqlStorage();
const instrumented = instrumentSqlStorage(mockSql);

sentryCore.startSpan({ name: 'parent' }, () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');

instrumented.exec('SELECT * FROM users WHERE id = ?', 42);

expect(startSpanSpy).toHaveBeenCalledTimes(startsSpan ? 1 : 0);
});

expect(sanitizeSpy).toHaveBeenCalledTimes(startsSpan ? 1 : 0);
expect(mockSql.exec).toHaveBeenCalledWith('SELECT * FROM users WHERE id = ?', 42);
});
});
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Queue } from '@cloudflare/workers-types';
import * as SentryCore from '@sentry/core';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { instrumentQueueProducer } from '../../../src/instrumentations/worker/instrumentQueueProducer';
import { initTestClient, resetSdk } from '../../testUtils';

function createMockQueue(): Queue {
return {
Expand Down Expand Up @@ -178,4 +179,30 @@ describe('instrumentQueueProducer', () => {
const wrapped = instrumentQueueProducer(queue, 'MY_QUEUE') as Queue & { customMethod: () => string };
expect(wrapped.customMethod()).toBe('hi');
});

describe('inside a parent span', () => {
afterEach(() => {
resetSdk();
});

test.each([
[1, true],
[0, false],
])('with tracesSampleRate %s, starts a span: %s', async (tracesSampleRate, startsSpan) => {
initTestClient({ tracesSampleRate });
const queue = createMockQueue();
const wrapped = instrumentQueueProducer(queue, 'MY_QUEUE');

await SentryCore.startSpan({ name: 'parent' }, () => {
const startSpanSpy = vi.spyOn(SentryCore, 'startSpan');

const result = wrapped.send({ hello: 'world' });

expect(startSpanSpy).toHaveBeenCalledTimes(startsSpan ? 1 : 0);
return result;
});

expect(queue.send).toHaveBeenLastCalledWith({ hello: 'world' }, undefined);
});
});
});
Loading