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
Expand Up @@ -31,6 +31,8 @@ it('Scheduled handler creates transaction with correct attributes', async ({ sig
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
[SENTRY_SEGMENT_NAME_SOURCE]: 'task',
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
'sentry.description': expect.stringMatching(/^Scheduled Cron/),
'code.function.name': 'scheduled',
'faas.cron': expect.any(String),
'faas.time': expect.any(String),
'faas.trigger': 'timer',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { EmailMessage } from '@cloudflare/workers-types';
import type { AnyExportedHandler } from '../../types';
import type { env as cloudflareEnv } from 'cloudflare:workers';
import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes';
import {
SENTRY_SEGMENT_NAME_SOURCE,
CODE_FUNCTION_NAME,
SENTRY_OP,
FAAS_TRIGGER,
SENTRY_ORIGIN,
} from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, withIsolationScope } from '@sentry/core';
import { captureException, startSpan, withIsolationScope } from '@sentry/core';
import type { CloudflareOptions } from '../../client';
import { flushAndDispose } from '../../flush';
import { ensureInstrumented } from '../../instrument';
Expand Down Expand Up @@ -35,11 +41,12 @@ function wrapEmailHandler(

return startSpan(
{
name: `Handle Email ${emailMessage.to}`,
name: 'email',
attributes: {
[SENTRY_OP]: FUNCTION,
'faas.trigger': 'email',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.email',
[CODE_FUNCTION_NAME]: 'email',
[FAAS_TRIGGER]: 'email',
[SENTRY_ORIGIN]: 'auto.faas.cloudflare.email',
Comment thread
cursor[bot] marked this conversation as resolved.
[SENTRY_SEGMENT_NAME_SOURCE]: 'task',
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import type { ScheduledController } from '@cloudflare/workers-types';
import type { AnyExportedHandler } from '../../types';
import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers';
import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes';
import {
SENTRY_SEGMENT_NAME_SOURCE,
CODE_FUNCTION_NAME,
SENTRY_OP,
FAAS_CRON,
FAAS_TIME,
FAAS_TRIGGER,
SENTRY_DESCRIPTION,
SENTRY_ORIGIN,
} from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, withIsolationScope } from '@sentry/core';
import { captureException, hasSpanStreamingEnabled, startSpan, withIsolationScope } from '@sentry/core';
import type { CloudflareOptions } from '../../client';
import { flushAndDispose } from '../../flush';
import { ensureInstrumented } from '../../instrument';
Expand All @@ -30,15 +39,21 @@ function wrapScheduledHandler(

addCloudResourceContext(isolationScope);

const description = `Scheduled Cron ${controller.cron}`;

return startSpan(
{
name: `Scheduled Cron ${controller.cron}`,
name: client && hasSpanStreamingEnabled(client) ? 'scheduled' : description,
attributes: {
[SENTRY_OP]: FUNCTION,
'faas.cron': controller.cron,
'faas.time': new Date(controller.scheduledTime).toISOString(),
'faas.trigger': 'timer',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
// override description inference by Relay to preserve the original (transaction-based) description.
// sentry-conventions can't map the special case for the "Scheduled Cron" prefix and the chron string.
[SENTRY_DESCRIPTION]: description,
[CODE_FUNCTION_NAME]: 'scheduled',
[FAAS_CRON]: controller.cron,
[FAAS_TIME]: new Date(controller.scheduledTime).toISOString(),
[FAAS_TRIGGER]: 'timer',
[SENTRY_ORIGIN]: 'auto.faas.cloudflare.scheduled',
[SENTRY_SEGMENT_NAME_SOURCE]: 'task',
},
},
Expand Down
10 changes: 6 additions & 4 deletions packages/cloudflare/src/wrapMethodWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { DurableObjectStorage } from '@cloudflare/workers-types';
import type { SerializedTraceData } from '@sentry/core';
import { CODE_FUNCTION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import {
isObjectLike,
captureException,
continueTrace,
isThenable,
type Scope,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startNewTrace as startNewTraceCore,
startSpan,
} from '@sentry/core';
Expand Down Expand Up @@ -203,8 +203,10 @@ export function wrapMethodWithSentry<T extends OriginalMethod>(

const attributes = wrapperOptions.spanOp
? {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: wrapperOptions.spanOp,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin,
[SENTRY_OP]: wrapperOptions.spanOp,
[SENTRY_ORIGIN]: origin,
// `function` spans are already named like their function name, so we just set `code.function.name` here.
...(wrapperOptions.spanOp === FUNCTION && { [CODE_FUNCTION_NAME]: methodName }),
}
: {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,15 @@ describe('instrumentEmail', () => {
const emailMessage = createMockEmailMessage();
await wrappedHandler.email?.(emailMessage, MOCK_ENV, createMockExecutionContext());

expect(sentryEvent.transaction).toEqual(`Handle Email ${emailMessage.to}`);
// The recipient is deliberately not carried over into a description: it is PII, and the span
// name must stay low cardinality.
expect(sentryEvent.transaction).toEqual('email');
expect(sentryEvent.spans).toHaveLength(0);
expect(sentryEvent.contexts?.trace).toEqual({
data: {
'sentry.origin': 'auto.faas.cloudflare.email',
'sentry.op': 'function',
'code.function.name': 'email',
'faas.trigger': 'email',
'sentry.sample_rate': 1,
'sentry.segment.name.source': 'task',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ describe('instrumentScheduled', () => {
data: {
'sentry.origin': 'auto.faas.cloudflare.scheduled',
'sentry.op': 'function',
'sentry.description': 'Scheduled Cron 0 0 0 * * *',
'code.function.name': 'scheduled',
Comment thread
cursor[bot] marked this conversation as resolved.
'faas.cron': '0 0 0 * * *',
'faas.time': expect.any(String),
'faas.trigger': 'timer',
Expand All @@ -272,6 +274,32 @@ describe('instrumentScheduled', () => {
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
});
});

async function spanNameFor(traceLifecycle: 'static' | 'stream'): Promise<string | undefined> {
let spanName: string | undefined;

const handler = {
scheduled(_controller, _env, _context) {
// Read the name while the handler is in flight: the gate applies at span start.
const activeSpan = SentryCore.getActiveSpan();
spanName = activeSpan ? SentryCore.spanToJSON(SentryCore.getRootSpan(activeSpan)).name : undefined;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;

const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, traceLifecycle }), handler);

await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());

return spanName;
}

test('keeps the cron out of the span name when span streaming is enabled', async () => {
expect(await spanNameFor('stream')).toBe('scheduled');
});

test('keeps the descriptive span name when span streaming is disabled', async () => {
expect(await spanNameFor('static')).toBe('Scheduled Cron 0 0 0 * * *');
});
});

test('flush must be called when all waitUntil are done', async () => {
Expand Down
44 changes: 44 additions & 0 deletions packages/cloudflare/test/wrapMethodWithSentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,50 @@ describe('wrapMethodWithSentry', () => {
);
});

it('sets code.function.name on function spans', async () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');
const handler = vi.fn().mockResolvedValue('result');
const options = {
origin: 'auto.faas.cloudflare.durable_object',
options: {},
context: createMockContext(),
spanName: 'fetch',
spanOp: 'function',
};

const wrapped = wrapMethodWithSentry(options, handler, 'fetch');
await wrapped();

expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({ 'code.function.name': 'fetch' }),
}),
expect.any(Function),
);
});

it('does not set code.function.name on spans with a different op', async () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');
const handler = vi.fn().mockResolvedValue('result');
const options = {
origin: 'auto.faas.cloudflare.durable_object',
options: {},
context: createMockContext(),
spanName: 'fetch',
spanOp: 'test-op',
};

const wrapped = wrapMethodWithSentry(options, handler, 'fetch');
await wrapped();

expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.not.objectContaining({ 'code.function.name': expect.anything() }),
}),
expect.any(Function),
);
});

it('does not create span when spanName is not provided', async () => {
const startSpanSpy = vi.spyOn(sentryCore, 'startSpan');
const handler = vi.fn().mockResolvedValue('result');
Expand Down
Loading