diff --git a/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts index 3b04189aea2f..9f689d35018b 100644 --- a/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts @@ -58,8 +58,10 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL expect(parent.parent_span_id).toBe(segment.span_id); expect(nested.parent_span_id).toBe(parent.span_id); for (const child of children) { - expect(getSpanOp(child)).toBe('function'); - expect(child.attributes['sentry.origin']?.value).toBe('auto.function.effect'); + // These spans come from user code calling `Effect.withSpan`, so the SDK does not claim an op or + // an `auto.*` origin for them. + expect(getSpanOp(child)).toBeUndefined(); + expect(child.attributes['sentry.origin']?.value).toBe('manual'); expect(child.trace_id).toBe(segment.trace_id); } }); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts index 7477d91a04be..2a468928c798 100644 --- a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts @@ -58,8 +58,10 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL expect(parent.parent_span_id).toBe(segment.span_id); expect(nested.parent_span_id).toBe(parent.span_id); for (const child of children) { - expect(getSpanOp(child)).toBe('function'); - expect(child.attributes['sentry.origin']?.value).toBe('auto.function.effect'); + // These spans come from user code calling `Effect.withSpan`, so the SDK does not claim an op or + // an `auto.*` origin for them. + expect(getSpanOp(child)).toBeUndefined(); + expect(child.attributes['sentry.origin']?.value).toBe('manual'); expect(child.trace_id).toBe(segment.trace_id); } }); diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts index 31ac0be35c29..08157a6e3a89 100644 --- a/packages/effect/src/tracer.ts +++ b/packages/effect/src/tracer.ts @@ -1,5 +1,5 @@ import { SENTRY_OP } from '@sentry/conventions/attributes'; -import { FUNCTION, HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; +import { HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; import type { Span, StartSpanOptions } from '@sentry/core'; import { isObjectLike, getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, withActiveSpan } from '@sentry/core'; import type * as Context from 'effect/Context'; @@ -7,20 +7,22 @@ import * as Exit from 'effect/Exit'; import * as Option from 'effect/Option'; import * as EffectTracer from 'effect/Tracer'; -function deriveOrigin(name: string): string { +function deriveOrigin(name: string): string | undefined { if (name.startsWith('http.server') || name.startsWith('http.client')) { return 'auto.http.effect'; } - return 'auto.function.effect'; + return undefined; } /** - * Effect span names are chosen by user code, so the name is the only signal available. `@effect/platform` - * names its HTTP spans `http.server`/`http.client`, which map onto the matching Sentry ops; everything - * else is arbitrary user work and falls back to `function`. + * Effect span names are chosen by whoever calls `Effect.withSpan`, so the name is the only signal + * available. `@effect/platform` names its HTTP spans `http.server`/`http.client`, which map onto the + * matching Sentry ops. Every other name comes from user code or a third-party library, whose semantics + * we cannot infer, so op and origin stay unset and the span keeps the core defaults: no op, and a + * `manual` origin. */ -function deriveOp(name: string): string { +function deriveOp(name: string): string | undefined { if (name.startsWith('http.server')) { return HTTP_SERVER; } @@ -29,7 +31,7 @@ function deriveOp(name: string): string { return HTTP_CLIENT; } - return FUNCTION; + return undefined; } type HrTime = [number, number]; @@ -190,12 +192,16 @@ function createSentrySpan( const parentSentrySpan = Option.isSome(parent) && isSentrySpan(parent.value) ? parent.value.sentrySpan : (getActiveSpan() ?? null); + const op = deriveOp(name); + const origin = deriveOrigin(name); + const newSpan = startInactiveSpan({ name, startTime: nanosToHrTime(startTime), + // Setting these to `undefined` would strip the core defaults instead of leaving them in place. attributes: { - [SENTRY_OP]: deriveOp(name), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: deriveOrigin(name), + ...(op && { [SENTRY_OP]: op }), + ...(origin && { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin }), }, ...(parentSentrySpan ? { parentSpan: parentSentrySpan } : {}), }); diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts index a056227576e1..90429b3a8bcd 100644 --- a/packages/effect/test/tracer.test.ts +++ b/packages/effect/test/tracer.test.ts @@ -192,12 +192,16 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ tracer, spanApi }) = return capturedAttributes; }).pipe(withSentryTracer); - it.effect('sets origin and op for regular spans', () => + // A name we cannot map belongs to user code or a third-party library. Leaving op and origin unset + // keeps the core defaults (no op, `manual` origin) rather than claiming we instrumented the span. + it.effect('leaves origin and op unset for spans it cannot map', () => Effect.gen(function* () { const attributes = yield* attributesFor('my-operation'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.function.effect'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('function'); + expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN); + expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_OP); }), );