Skip to content
Open
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 @@ -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);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
26 changes: 16 additions & 10 deletions packages/effect/src/tracer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
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';
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;
}
Expand All @@ -29,7 +31,7 @@ function deriveOp(name: string): string {
return HTTP_CLIENT;
}

return FUNCTION;
return undefined;
}

type HrTime = [number, number];
Expand Down Expand Up @@ -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 } : {}),
});
Expand Down
10 changes: 7 additions & 3 deletions packages/effect/test/tracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}),
);

Expand Down
Loading