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
@@ -1,6 +1,10 @@
// @ts-check
import * as Sentry from '@sentry/effect';
import { Cause, Effect, Layer, Logger, LogLevel, Runtime } from 'effect';
import * as Logger from 'effect/Logger';
import * as Layer from 'effect/Layer';
import * as Runtime from 'effect/Runtime';
import * as LogLevel from 'effect/LogLevel';
import * as Effect from 'effect/Effect';

const LogLevelLive = Logger.minimumLogLevel(LogLevel.Debug);
const AppLayer = Layer.mergeAll(
Expand All @@ -16,8 +20,9 @@ const AppLayer = Layer.mergeAll(
environment: 'qa',
tunnel: 'http://localhost:3031',
enableLogs: true,
enableEffectLogs: true,
}),
Layer.setTracer(Sentry.SentryEffectTracer),
Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger),
LogLevelLive,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@ test('captures Effect spans with correct parent-child structure', async ({ page
expect(spans).toContainEqual(
expect.objectContaining({
description: 'custom-effect-span',
data: expect.objectContaining({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: just checking, no action required: Does effect add any other attributes we could/should assert on?

(also, removing the op here sounds correct to me. Span kind doesn't map well to sentry ops and there's no internal op defined)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added couple of more assertions to this specific test and removed the .toContainEqual in the file completely.

'sentry.op': 'internal',
}),
}),
);

Expand Down
27 changes: 17 additions & 10 deletions dev-packages/e2e-tests/test-applications/effect-node/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import * as Sentry from '@sentry/effect';
import { HttpRouter, HttpServer, HttpServerResponse } from '@effect/platform';
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
import { Cause, Effect, Layer, Logger, LogLevel } from 'effect';
import * as Effect from 'effect/Effect';
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 { createServer } from 'http';

const SentryLive = Sentry.effectLayer({
dsn: process.env.E2E_TEST_DSN,
environment: 'qa',
debug: !!process.env.DEBUG,
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
enableLogs: true,
enableEffectLogs: true,
});
const SentryLive = Layer.mergeAll(
Sentry.effectLayer({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a fine setup and naming is also fine. To me (as an Effect noob), this signals that Sentry.effectLayer is the main one and the others are additional components. As long as we document this correctly, we should be good.

I have a question though: Does registering just the Sentry.effectLayer initialize error monitoring? This would be ideal, since errors should be part of the minimal Sentry setup. I believe you said for v4, we'll need an additional component for errors, which is also fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does registering just the Sentry.effectLayer initialize error monitoring?

Theoretically it would do everything a Sentry.init would do, so if the process crashes or someone would throw an unhandled error then yes. Once we get support for the ErrorReporter in v4, we could think of auto adding this or adding it manually with the layer (currently leaning towards auto adding, but this is another discussion)

dsn: process.env.E2E_TEST_DSN,
environment: 'qa',
debug: !!process.env.DEBUG,
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
enableLogs: true,
}),
Layer.setTracer(Sentry.SentryEffectTracer),
Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger),
);

const router = HttpRouter.empty.pipe(
HttpRouter.get('/test-success', HttpServerResponse.json({ version: 'v1' })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,97 @@ import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends an HTTP transaction', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ambiguous E2E test transaction filters match wrong transactions

Medium Severity

The "Sends an HTTP transaction" and "Sends transaction for error route" tests both use the identical filter transactionEvent?.transaction === 'http.server GET' with no route-specific distinguishing criteria. Previously, filters included route paths like /test-success and /test-error. Now any http.server GET transaction satisfies both filters, making these tests flaky and unable to verify they received the transaction for the correct route.

Additional Locations (1)
Fix in Cursor Fix in Web

transactionEvent?.transaction?.includes('/test-success')
);
return transactionEvent?.transaction === 'http.server GET';
});

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.transaction).toContain('/test-success');
expect(transactionEvent.transaction).toBe('http.server GET');
});

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

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.transaction).toContain('/test-transaction');
expect(transactionEvent.transaction).toBe('http.server GET');

const spans = transactionEvent.spans || [];
expect(spans).toContainEqual(
expect(spans).toEqual([
expect.objectContaining({
description: 'test-span',
}),
);
]);
});

test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction?.includes('/test-effect-span')
transactionEvent?.transaction === 'http.server GET' &&
transactionEvent?.spans?.some(span => span.description === 'custom-effect-span')
);
});

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.transaction).toContain('/test-effect-span');

const spans = transactionEvent.spans || [];
expect(transactionEvent.transaction).toBe('http.server GET');

expect(spans).toContainEqual(
expect(transactionEvent).toEqual(
expect.objectContaining({
description: 'custom-effect-span',
op: 'internal',
contexts: expect.objectContaining({
trace: expect.objectContaining({
origin: 'auto.http.effect',
}),
}),
spans: [
expect.objectContaining({
description: 'custom-effect-span',
origin: 'auto.function.effect',
}),
expect.objectContaining({
description: 'nested-span',
origin: 'auto.function.effect',
}),
],
sdk: expect.objectContaining({
name: 'sentry.javascript.effect',
packages: [
expect.objectContaining({
name: 'npm:@sentry/effect',
}),
expect.objectContaining({
name: 'npm:@sentry/node-light',
}),
],
}),
}),
);

expect(spans).toContainEqual(
expect.objectContaining({
description: 'nested-span',
}),
);
const parentSpan = transactionEvent.spans?.[0]?.span_id;
const nestedSpan = transactionEvent.spans?.[1]?.parent_span_id;

const parentSpan = spans.find(s => s.description === 'custom-effect-span');
const nestedSpan = spans.find(s => s.description === 'nested-span');
expect(nestedSpan?.parent_span_id).toBe(parentSpan?.span_id);
expect(nestedSpan).toBe(parentSpan);
});

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

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.transaction).toContain('/test-error');
expect(transactionEvent.transaction).toBe('http.server GET');
});
31 changes: 15 additions & 16 deletions packages/effect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,29 @@ This SDK does not have docs yet. Stay tuned.
```typescript
import * as Sentry from '@sentry/effect/server';
import { NodeRuntime } from '@effect/platform-node';
import { Layer } from 'effect';
import { Layer, Logger } from 'effect';
import { HttpLive } from './Http.js';

const MainLive = HttpLive.pipe(
Layer.provide(
Sentry.effectLayer({
dsn: '__DSN__',
tracesSampleRate: 1.0,
enableLogs: true,
enableEffectLogs: true,
enableEffectMetrics: true,
}),
),
const SentryLive = Layer.mergeAll(
Sentry.effectLayer({
dsn: '__DSN__',
tracesSampleRate: 1.0,
enableLogs: true,
}),
Layer.setTracer(Sentry.SentryEffectTracer),
Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: should we add the metrics layer here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well said. Will add

Sentry.SentryEffectMetricsLayer,
);

const MainLive = HttpLive.pipe(Layer.provide(SentryLive));
MainLive.pipe(Layer.launch, NodeRuntime.runMain);
```

The `effectLayer` function initializes Sentry and returns an Effect Layer that provides:
The `effectLayer` function initializes Sentry. To enable Effect instrumentation, compose with:

- Distributed tracing with automatic HTTP header extraction/injection
- Effect spans traced as Sentry spans
- Effect logs forwarded to Sentry (when `enableEffectLogs` is set)
- Effect metrics sent to Sentry (when `enableEffectMetrics` is set)
- `Layer.setTracer(Sentry.SentryEffectTracer)` - Effect spans traced as Sentry spans
- `Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger)` - Effect logs forwarded to Sentry
- `Sentry.SentryEffectMetricsLayer` - Effect metrics sent to Sentry

## Links

Expand Down
32 changes: 17 additions & 15 deletions packages/effect/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
import type { BrowserOptions } from '@sentry/browser';
import type * as EffectLayer from 'effect/Layer';
import { suspend as suspendLayer } from 'effect/Layer';
import type { EffectLayerBaseOptions } from '../utils/buildEffectLayer';
import { buildEffectLayer } from '../utils/buildEffectLayer';
import { empty as emptyLayer, suspend as suspendLayer } from 'effect/Layer';
import { init } from './sdk';

export { init } from './sdk';

/**
* Options for the Sentry Effect client layer.
*/
export type EffectClientLayerOptions = BrowserOptions & EffectLayerBaseOptions;
export type EffectClientLayerOptions = BrowserOptions;

/**
* Creates an Effect Layer that initializes Sentry for browser clients.
*
* This layer provides Effect applications with full Sentry instrumentation including:
* - Effect spans traced as Sentry spans
* - Effect logs forwarded to Sentry (when `enableEffectLogs` is set)
* - Effect metrics sent to Sentry (when `enableEffectMetrics` is set)
* To enable Effect tracing, logs, or metrics, compose with the respective layers:
* - `Layer.setTracer(Sentry.SentryEffectTracer)` for tracing
* - `Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger)` for logs
* - `Sentry.SentryEffectMetricsLayer` for metrics
*
* @example
* ```typescript
* import * as Sentry from '@sentry/effect/client';
* import { Layer, Effect } from 'effect';
* import { Layer, Logger, LogLevel } from 'effect';
*
* const ApiClientWithSentry = ApiClientLive.pipe(
* Layer.provide(Sentry.effectLayer({
* const SentryLive = Layer.mergeAll(
* Sentry.effectLayer({
* dsn: '__DSN__',
* integrations: [Sentry.browserTracingIntegration()],
* tracesSampleRate: 1.0,
* })),
* }),
* Layer.setTracer(Sentry.SentryEffectTracer),
* Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger),
* );
*
* Effect.runPromise(Effect.provide(myEffect, ApiClientWithSentry));
* ```
*/
export function effectLayer(options: EffectClientLayerOptions): EffectLayer.Layer<never, never, never> {
return suspendLayer(() => buildEffectLayer(options, init(options)));
return suspendLayer(() => {
init(options);

return emptyLayer;
});
}
31 changes: 16 additions & 15 deletions packages/effect/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
import type { NodeOptions } from '@sentry/node-core/light';
import type * as EffectLayer from 'effect/Layer';
import type { EffectLayerBaseOptions } from '../utils/buildEffectLayer';
import { buildEffectLayer } from '../utils/buildEffectLayer';
import { empty as emptyLayer, suspend as suspendLayer } from 'effect/Layer';
import { init } from './sdk';

export { init } from './sdk';

/**
* Options for the Sentry Effect server layer.
*/
export type EffectServerLayerOptions = NodeOptions & EffectLayerBaseOptions;
export type EffectServerLayerOptions = NodeOptions;

/**
* Creates an Effect Layer that initializes Sentry for Node.js servers.
*
* This layer provides Effect applications with full Sentry instrumentation including:
* - Effect spans traced as Sentry spans
* - Effect logs forwarded to Sentry (when `enableEffectLogs` is set)
* - Effect metrics sent to Sentry (when `enableEffectMetrics` is set)
* To enable Effect tracing, logs, or metrics, compose with the respective layers:
* - `Layer.setTracer(Sentry.SentryEffectTracer)` for tracing
* - `Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger)` for logs
* - `Sentry.SentryEffectMetricsLayer` for metrics
*
* @example
* ```typescript
* import * as Sentry from '@sentry/effect/server';
* import { NodeRuntime } from '@effect/platform-node';
* import { Layer } from 'effect';
* import { Layer, Logger } from 'effect';
* import { HttpLive } from './Http.js';
*
* const MainLive = HttpLive.pipe(
* Layer.provide(Sentry.effectLayer({
* dsn: '__DSN__',
* enableEffectLogs: true,
* enableEffectMetrics: true,
* })),
* const SentryLive = Layer.mergeAll(
* Sentry.effectLayer({ dsn: '__DSN__' }),
* Layer.setTracer(Sentry.SentryEffectTracer),
* Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger),
* );
*
* const MainLive = HttpLive.pipe(Layer.provide(SentryLive));
* MainLive.pipe(Layer.launch, NodeRuntime.runMain);
* ```
*/
export function effectLayer(options: EffectServerLayerOptions): EffectLayer.Layer<never, never, never> {
return buildEffectLayer(options, init(options));
return suspendLayer(() => {
init(options);
return emptyLayer;
});
}
Loading
Loading