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 @@ -3,29 +3,28 @@ import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends an HTTP transaction', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
return transactionEvent?.transaction === 'http.server GET';
return transactionEvent?.transaction === 'GET';
});

await fetch(`${baseURL}/test-success`);

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.transaction).toBe('http.server GET');
expect(transactionEvent.transaction).toBe('GET');
});

test('Sends transaction with manual Effect span', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
return (
transactionEvent?.transaction === 'http.server GET' &&
transactionEvent?.spans?.some(span => span.description === 'test-span')
transactionEvent?.transaction === 'GET' && transactionEvent?.spans?.some(span => span.description === 'test-span')
);
});

await fetch(`${baseURL}/test-transaction`);

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.transaction).toBe('http.server GET');
expect(transactionEvent.transaction).toBe('GET');

const spans = transactionEvent.spans || [];
expect(spans).toEqual([
Expand All @@ -38,7 +37,7 @@ test('Sends transaction with manual Effect span', async ({ baseURL }) => {
test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
return (
transactionEvent?.transaction === 'http.server GET' &&
transactionEvent?.transaction === 'GET' &&
transactionEvent?.spans?.some(span => span.description === 'custom-effect-span')
);
});
Expand All @@ -47,7 +46,7 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.transaction).toBe('http.server GET');
expect(transactionEvent.transaction).toBe('GET');

expect(transactionEvent).toEqual(
expect.objectContaining({
Expand Down Expand Up @@ -88,12 +87,12 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL

test('Sends transaction for error route', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
return transactionEvent?.transaction === 'http.server GET';
return transactionEvent?.transaction === 'GET';
});

await fetch(`${baseURL}/test-error`);

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.transaction).toBe('http.server GET');
expect(transactionEvent.transaction).toBe('GET');
});
16 changes: 13 additions & 3 deletions packages/effect/src/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ import * as Exit from 'effect/Exit';
import * as Option from 'effect/Option';
import * as EffectTracer from 'effect/Tracer';

function deriveOrigin(name: string): string {
if (name.startsWith('http.server') || name.startsWith('http.client')) {
const HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']);

/**
* 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).
*/
function deriveOrigin(name: string, kind: EffectTracer.SpanKind): string {
if (
name.startsWith('http.server') ||
name.startsWith('http.client') ||
((kind === 'server' || kind === 'client') && HTTP_METHODS.has(name))
) {
return 'auto.http.effect';
}

Expand Down Expand Up @@ -172,7 +182,7 @@ function createSentrySpan(
name,
startTime: nanosToHrTime(startTime),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: deriveOrigin(name),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: deriveOrigin(name, kind),
},
...(parentSentrySpan ? { parentSpan: parentSentrySpan } : {}),
});
Expand Down
75 changes: 75 additions & 0 deletions packages/effect/test/tracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,81 @@ describe('SentryEffectTracer', () => {
}).pipe(withSentryTracer),
);

it.effect('sets origin to auto.http.effect for server spans named after the request method', () =>
Effect.gen(function* () {
let capturedAttributes: Record<string, unknown> | undefined;

const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => {
capturedAttributes = options.attributes;
return {
spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }),
isRecording: () => true,
setAttribute: vi.fn(),
setStatus: vi.fn(),
addEvent: vi.fn(),
end: vi.fn(),
} as unknown as sentryCore.Span;
});

yield* Effect.withSpan('GET', { kind: 'server' })(Effect.succeed('ok'));

expect(capturedAttributes).toBeDefined();
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect');

mockStartInactiveSpan.mockRestore();
}).pipe(withSentryTracer),
);

it.effect('sets origin to auto.http.effect for client spans named after the request method', () =>
Effect.gen(function* () {
let capturedAttributes: Record<string, unknown> | undefined;

const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => {
capturedAttributes = options.attributes;
return {
spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }),
isRecording: () => true,
setAttribute: vi.fn(),
setStatus: vi.fn(),
addEvent: vi.fn(),
end: vi.fn(),
} as unknown as sentryCore.Span;
});

yield* Effect.withSpan('POST', { kind: 'client' })(Effect.succeed('ok'));

expect(capturedAttributes).toBeDefined();
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect');

mockStartInactiveSpan.mockRestore();
}).pipe(withSentryTracer),
);

it.effect('sets origin to auto.function.effect for internal spans named after a request method', () =>
Effect.gen(function* () {
let capturedAttributes: Record<string, unknown> | undefined;

const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => {
capturedAttributes = options.attributes;
return {
spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }),
isRecording: () => true,
setAttribute: vi.fn(),
setStatus: vi.fn(),
addEvent: vi.fn(),
end: vi.fn(),
} as unknown as sentryCore.Span;
});

yield* Effect.withSpan('GET')(Effect.succeed('ok'));

expect(capturedAttributes).toBeDefined();
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.function.effect');

mockStartInactiveSpan.mockRestore();
}).pipe(withSentryTracer),
);

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