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
26 changes: 26 additions & 0 deletions dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as Cause from 'effect/Cause';
import * as Layer from 'effect/Layer';
import * as Logger from 'effect/Logger';
import * as LogLevel from 'effect/LogLevel';
import * as Tracer from 'effect/Tracer';
import { createServer } from 'http';

const SentryLive = Layer.mergeAll(
Expand Down Expand Up @@ -42,6 +43,31 @@ const router = HttpRouter.empty.pipe(
}),
),

HttpRouter.get(
'/test-root-span',
Effect.gen(function* () {
yield* Effect.void.pipe(Effect.withSpan('root-span-request-marker'));
yield* Effect.sleep('10 millis').pipe(Effect.withSpan('detached-root-span', { root: true }));
return yield* HttpServerResponse.json({ status: 'ok' });
}),
),

HttpRouter.get(
'/test-external-parent',
Effect.gen(function* () {
yield* Effect.sleep('10 millis').pipe(
Effect.withSpan('continued-span', {
parent: Tracer.externalSpan({
traceId: 'fedcba0987654321fedcba0987654321',
spanId: '0987654321fedcba',
sampled: true,
}),
}),
);
return yield* HttpServerResponse.json({ status: 'ok' });
}).pipe(Effect.provide(Sentry.SentryEffectExternalSpanLayer)),
),

HttpRouter.get(
'/test-error',
Effect.gen(function* () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,41 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
expect(child.trace_id).toBe(segment.trace_id);
}
});

test('Sends a root: true span as its own segment in a new trace', async ({ baseURL }) => {
const requestSpansPromise = collectStreamedSpans(
'effect-3-node',
spans =>
spans.some(
span =>
span.is_segment &&
getSpanOp(span) === 'http.server' &&
span.attributes['url.path']?.value === '/test-root-span',
) && spans.some(span => span.name === 'root-span-request-marker'),
);
const detachedSpanPromise = waitForStreamedSpan('effect-3-node', span => span.name === 'detached-root-span');

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

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(requestSpans.filter(span => !span.is_segment).map(span => span.name)).toEqual(['root-span-request-marker']);

expect(detachedSpan.is_segment).toBe(true);
expect(detachedSpan.parent_span_id).toBeUndefined();
expect(detachedSpan.trace_id).not.toBe(segment.trace_id);
});

test('Continues the trace of a Tracer.externalSpan parent with the external span layer', async ({ baseURL }) => {
const spanPromise = waitForStreamedSpan('effect-3-node', span => span.name === 'continued-span');

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

const span = await spanPromise;
expect(span).toMatchObject({
is_segment: true,
trace_id: 'fedcba0987654321fedcba0987654321',
parent_span_id: '0987654321fedcba',
});
});
27 changes: 27 additions & 0 deletions dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,33 @@ const Routes = Layer.mergeAll(
}),
),

HttpRouter.add(
'GET',
'/test-root-span',
Effect.gen(function* () {
yield* Effect.void.pipe(Effect.withSpan('root-span-request-marker'));
yield* Effect.sleep('10 millis').pipe(Effect.withSpan('detached-root-span', { root: true }));
return yield* HttpServerResponse.json({ status: 'ok' });
}),
),

HttpRouter.add(
'GET',
'/test-external-parent',
Effect.gen(function* () {
yield* Effect.sleep('10 millis').pipe(
Effect.withSpan('continued-span', {
parent: Tracer.externalSpan({
traceId: 'fedcba0987654321fedcba0987654321',
spanId: '0987654321fedcba',
sampled: true,
}),
}),
);
return yield* HttpServerResponse.json({ status: 'ok' });
}).pipe(Effect.provide(Sentry.SentryEffectExternalSpanLayer)),
),

HttpRouter.add(
'GET',
'/test-error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,41 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
expect(child.trace_id).toBe(segment.trace_id);
}
});

test('Sends a root: true span as its own segment in a new trace', async ({ baseURL }) => {
const requestSpansPromise = collectStreamedSpans(
'effect-4-node',
spans =>
spans.some(
span =>
span.is_segment &&
getSpanOp(span) === 'http.server' &&
span.attributes['url.path']?.value === '/test-root-span',
) && spans.some(span => span.name === 'root-span-request-marker'),
);
const detachedSpanPromise = waitForStreamedSpan('effect-4-node', span => span.name === 'detached-root-span');

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

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(requestSpans.filter(span => !span.is_segment).map(span => span.name)).toEqual(['root-span-request-marker']);

expect(detachedSpan.is_segment).toBe(true);
expect(detachedSpan.parent_span_id).toBeUndefined();
expect(detachedSpan.trace_id).not.toBe(segment.trace_id);
});

test('Continues the trace of a Tracer.externalSpan parent with the external span layer', async ({ baseURL }) => {
const spanPromise = waitForStreamedSpan('effect-4-node', span => span.name === 'continued-span');

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

const span = await spanPromise;
expect(span).toMatchObject({
is_segment: true,
trace_id: 'fedcba0987654321fedcba0987654321',
parent_span_id: '0987654321fedcba',
});
});
2 changes: 1 addition & 1 deletion packages/effect/src/client/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ import { makeSentryTracer } from '../tracer';
* keeps the browser variant substitutable (mocks, bundler interop) exactly as it was when the tracer
* called it directly.
*/
export const SentryEffectTracer = makeSentryTracer(options => startInactiveSpan(options));
export const SentryEffectTracer = makeSentryTracer(options => startInactiveSpan(options), false);
1 change: 1 addition & 0 deletions packages/effect/src/index.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export { effectLayer, init } from './client/index';
export type { EffectClientLayerOptions } from './client/index';

export { SentryEffectTracer } from './client/tracer';
export { SentryEffectExternalSpanLayer } from './tracer';
export { SentryEffectLogger } from './logger';
export { SentryEffectMetricsLayer } from './metrics';
1 change: 1 addition & 0 deletions packages/effect/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export { effectLayer, init } from './server/index';
export type { EffectServerLayerOptions } from './server/index';

export { SentryEffectTracer } from './server/tracer';
export { SentryEffectExternalSpanLayer } from './tracer';
export { SentryEffectLogger } from './logger';
export { SentryEffectMetricsLayer } from './metrics';
6 changes: 5 additions & 1 deletion packages/effect/src/server/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { makeSentryTracer } from '../tracer';
* every span start with a `getClient()` lookup to lazily install `spanStreamingIntegration`, which on
* the server is pure overhead — `ServerRuntimeClient` already installs it eagerly.
*
* Every parentless Effect span starts a new trace. A server process forks no propagation context on
* its own, so without this every request, reactor and background job of the process would land in
* one trace.
*
* See `./client/tracer.ts` for why the call is wrapped in an arrow.
*/
export const SentryEffectTracer = makeSentryTracer(options => startInactiveSpan(options));
export const SentryEffectTracer = makeSentryTracer(options => startInactiveSpan(options), true);
Loading
Loading