diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts index a09128d28fc0..7043172e6284 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts @@ -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', diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts index 5c54f03b2531..11a8fb9b419d 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts @@ -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'; @@ -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', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts b/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts index f633770988f9..b0da3a279a39 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts @@ -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'; @@ -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', }, }, diff --git a/packages/cloudflare/src/wrapMethodWithSentry.ts b/packages/cloudflare/src/wrapMethodWithSentry.ts index 95a69473f667..d20fd184d100 100644 --- a/packages/cloudflare/src/wrapMethodWithSentry.ts +++ b/packages/cloudflare/src/wrapMethodWithSentry.ts @@ -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'; @@ -203,8 +203,10 @@ export function wrapMethodWithSentry( 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 }), } : {}; diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts index c1222a14c605..2c15f5a39966 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts @@ -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', diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts index 6a2dd9e08dbe..46090684daea 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts @@ -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', 'faas.cron': '0 0 0 * * *', 'faas.time': expect.any(String), 'faas.trigger': 'timer', @@ -272,6 +274,32 @@ describe('instrumentScheduled', () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); }); + + async function spanNameFor(traceLifecycle: 'static' | 'stream'): Promise { + 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; + + 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 () => { diff --git a/packages/cloudflare/test/wrapMethodWithSentry.test.ts b/packages/cloudflare/test/wrapMethodWithSentry.test.ts index 45652c6f26e5..53f30bde5950 100644 --- a/packages/cloudflare/test/wrapMethodWithSentry.test.ts +++ b/packages/cloudflare/test/wrapMethodWithSentry.test.ts @@ -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');