Skip to content
Merged
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 @@ -12,7 +12,7 @@ import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-in
await fetch(`${baseURL}/${route}`);

const span = await spanPromise;
expect(span.name).toBe('http.server GET');
expect(span.name).toBe('GET');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.effect');
});
});
Expand All @@ -30,7 +30,7 @@ test('Sends a manual Effect span', async ({ baseURL }) => {
const spans = await spansPromise;
const segment = spans.find(span => span.is_segment)!;
const children = spans.filter(span => !span.is_segment);
expect(segment.name).toBe('http.server GET');
expect(segment.name).toBe('GET');
expect(children).toHaveLength(1);
expect(children[0]).toMatchObject({ name: 'test-span', parent_span_id: segment.span_id });
});
Expand All @@ -49,7 +49,7 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
const spans = await spansPromise;
const segment = spans.find(span => span.is_segment)!;
const children = spans.filter(span => !span.is_segment);
expect(segment.name).toBe('http.server GET');
expect(segment.name).toBe('GET');
expect(segment.attributes['sentry.origin']?.value).toBe('auto.http.effect');
expect(segment.attributes['sentry.sdk.name']?.value).toBe('sentry.javascript.effect');
expect(children).toHaveLength(2);
Expand Down Expand Up @@ -83,7 +83,7 @@ test('Sends a root: true span as its own segment in a new trace', async ({ baseU

const [requestSpans, detachedSpan] = await Promise.all([requestSpansPromise, detachedSpanPromise]);
const segment = requestSpans.find(span => span.is_segment && getSpanOp(span) === 'http.server')!;
expect(segment.name).toBe('http.server GET');
expect(segment.name).toBe('GET');
expect(requestSpans.filter(span => !span.is_segment).map(span => span.name)).toEqual(['root-span-request-marker']);

expect(detachedSpan.is_segment).toBe(true);
Expand Down
31 changes: 13 additions & 18 deletions packages/effect/src/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ import { succeed as succeedLayer } from 'effect/Layer';
import * as Option from 'effect/Option';
import * as EffectTracer from 'effect/Tracer';

function deriveOrigin(name: string): string | undefined {
if (name.startsWith('http.server') || name.startsWith('http.client')) {
return 'auto.http.effect';
}

return undefined;
}

const EFFECT_SPAN_SYMBOL = Symbol.for('@sentry/effect.EffectSpan');

function markEffectSpan(span: Span): void {
Expand All @@ -43,19 +35,22 @@ function isEffectSpan(span: Span): boolean {
return (span as { [EFFECT_SPAN_SYMBOL]?: boolean })[EFFECT_SPAN_SYMBOL] === true;
}

const HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']);

/**
* 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.
* Effect span names are chosen by whoever calls `Effect.withSpan`, so the name and the kind are the only
* signals available. Effect names its HTTP spans `http.server <method>`/`http.client <method>` (Effect v3
* and v4 before 4.0.2) or only `<method>` with a `server`/`client` kind (Effect 4.0.2 and later), which map
* onto the matching Sentry ops. Every other span 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 | undefined {
if (name.startsWith('http.server')) {
function deriveOp(name: string, kind: EffectTracer.SpanKind): string | undefined {
if (name.startsWith('http.server') || (kind === 'server' && HTTP_METHODS.has(name))) {
return HTTP_SERVER;
}

if (name.startsWith('http.client')) {
if (name.startsWith('http.client') || (kind === 'client' && HTTP_METHODS.has(name))) {
return HTTP_CLIENT;
}

Expand Down Expand Up @@ -390,8 +385,8 @@ function createSentrySpan(
startTime: bigint,
kind: EffectTracer.SpanKind,
): SentrySpanLike {
const op = deriveOp(name);
const origin = deriveOrigin(name);
const op = deriveOp(name, kind);
const origin = op && 'auto.http.effect';

// Effect calls the tracer when the span starts, so we start it on Sentry's clock and convert later Effect times
// relative to this (see `_toSentryTime`).
Expand Down
31 changes: 29 additions & 2 deletions packages/effect/test/tracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa
);

/** Captures the attributes the tracer passes to `startInactiveSpan` for a given Effect span name. */
const attributesFor = (spanName: string) =>
const attributesFor = (spanName: string, kind?: Tracer.SpanKind) =>
Effect.gen(function* () {
let capturedAttributes: Record<string, unknown> | undefined;

Expand All @@ -191,7 +191,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa
return mockSpan();
});

yield* Effect.withSpan(spanName)(Effect.succeed('ok'));
yield* Effect.withSpan(spanName, { kind })(Effect.succeed('ok'));

return capturedAttributes;
}).pipe(withSentryTracer);
Expand Down Expand Up @@ -261,6 +261,33 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa
}),
);

it.effect('sets origin and op for server spans named after the request method', () =>
Effect.gen(function* () {
const attributes = yield* attributesFor('GET', 'server');

expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect');
expect(attributes?.[SENTRY_OP]).toBe('http.server');
}),
);

it.effect('sets origin and op for client spans named after the request method', () =>
Effect.gen(function* () {
const attributes = yield* attributesFor('POST', 'client');

expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect');
expect(attributes?.[SENTRY_OP]).toBe('http.client');
}),
);

it.effect('leaves origin and op unset for internal spans named after a request method', () =>
Effect.gen(function* () {
const attributes = yield* attributesFor('GET');

expect(attributes).not.toHaveProperty(SENTRY_ORIGIN);
expect(attributes).not.toHaveProperty(SENTRY_OP);
}),
);

it.effect('can be used with Effect.withTracer', () =>
Effect.gen(function* () {
const result = yield* Effect.withSpan('inline-tracer-span')(Effect.succeed('with-tracer'));
Expand Down
Loading