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
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 @@ -63,3 +63,59 @@ 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',
});
});

test('Ignores an incoming traceparent header without the external span layer', async ({ baseURL }) => {
const traceId = '1234567890abcdef1234567890abcdef';
const parentSpanId = 'abcdef1234567890';

const spanPromise = waitForStreamedSpan(
'effect-3-node',
span =>
span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/test-success',
);

await fetch(`${baseURL}/test-success`, { headers: { traceparent: `00-${traceId}-${parentSpanId}-01` } });

const span = await spanPromise;
expect(span.name).toBe('http.server GET');
expect(span.trace_id).not.toBe(traceId);
expect(span.parent_span_id).toBeUndefined();
});
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 @@ -46,6 +46,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 @@ -63,3 +63,59 @@ 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',
});
});

test('Ignores an incoming traceparent header without the external span layer', async ({ baseURL }) => {
const traceId = '1234567890abcdef1234567890abcdef';
const parentSpanId = 'abcdef1234567890';

const spanPromise = waitForStreamedSpan(
'effect-4-node',
span =>
span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/test-success',
);

await fetch(`${baseURL}/test-success`, { headers: { traceparent: `00-${traceId}-${parentSpanId}-01` } });

const span = await spanPromise;
expect(span.name).toBe('http.server GET');
expect(span.trace_id).not.toBe(traceId);
expect(span.parent_span_id).toBeUndefined();
});
27 changes: 27 additions & 0 deletions packages/effect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ const HttpLive = HttpRouter.serve(Routes).pipe(
NodeRuntime.runMain(Layer.launch(HttpLive));
```

## Continuing external traces

The tracer ignores `Tracer.externalSpan` parents by default, so Sentry's own
trace continuation stays in charge. This includes the parent that
`@effect/platform` builds from an incoming `traceparent` or `b3` header. To
continue the trace of an external span instead, provide
`SentryEffectExternalSpanLayer`. Next to the tracer layer it applies to every
span in the runtime, including the HTTP server spans. On Effect v4, replace the
`Layer.setTracer` line with the `Layer.succeed` call from the example above.

```typescript
const SentryLive = Layer.mergeAll(
Sentry.effectLayer({ dsn: '__DSN__', tracesSampleRate: 1.0 }),
Layer.setTracer(Sentry.SentryEffectTracer),
Sentry.SentryEffectExternalSpanLayer,
);
```

Provided to a single effect, it applies to that effect only:

```typescript
const processJob = handleMessage(message).pipe(
Effect.withSpan('process-job', { parent: Tracer.externalSpan(message.trace) }),
Effect.provide(Sentry.SentryEffectExternalSpanLayer),
);
```

## Links

- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/effect/)
Expand Down
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