Skip to content

Commit 194e173

Browse files
feat(node): Add Effect integration for tracing Effect computations
Co-Authored-By: Jan Peer Stöcklmair <jan.peer@sentry.io>
1 parent b7f8cfe commit 194e173

6 files changed

Lines changed: 544 additions & 0 deletions

File tree

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export { anthropicAIIntegration } from './integrations/tracing/anthropic-ai';
2929
export { googleGenAIIntegration } from './integrations/tracing/google-genai';
3030
export { langChainIntegration } from './integrations/tracing/langchain';
3131
export { langGraphIntegration } from './integrations/tracing/langgraph';
32+
export { effectIntegration } from './integrations/tracing/effect';
3233
export {
3334
launchDarklyIntegration,
3435
buildLaunchDarklyFlagUsedHandler,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import type { IntegrationFn } from '@sentry/core';
2+
import { captureException, defineIntegration, getCurrentScope, startSpan, withScope } from '@sentry/core';
3+
import type { EffectExit, EffectModule, EffectSpan, EffectTracer } from './types';
4+
5+
const INTEGRATION_NAME = 'Effect';
6+
7+
// Global state to track if tracing is enabled
8+
let isInstrumentationEnabled = false;
9+
let originalTracer: EffectTracer | undefined;
10+
11+
/**
12+
* Instruments Effect spans to create Sentry spans.
13+
*/
14+
export const instrumentEffect = Object.assign(
15+
(): void => {
16+
if (isInstrumentationEnabled) {
17+
return;
18+
}
19+
20+
try {
21+
// eslint-disable-next-line @typescript-eslint/no-var-requires
22+
const Effect = require('effect') as EffectModule;
23+
24+
if (!Effect?.Tracer) {
25+
return;
26+
}
27+
28+
// Store the original tracer if it exists
29+
originalTracer = Effect.Tracer.get?.() || Effect.Tracer.current?.() || undefined;
30+
31+
// Create our custom tracer that wraps operations in Sentry spans
32+
const sentryTracer: EffectTracer = {
33+
onSpanStart(span: EffectSpan) {
34+
// Hook for span start - can be used for additional instrumentation
35+
if (originalTracer?.onSpanStart) {
36+
originalTracer.onSpanStart(span);
37+
}
38+
},
39+
40+
onSpanEnd(span: EffectSpan, exit: EffectExit) {
41+
// Hook for span end - handle failures
42+
if (exit._tag === 'Failure' && exit.cause) {
43+
withScope(scope => {
44+
scope.setTag('effect.exit_tag', exit._tag);
45+
scope.setContext('effect.span', {
46+
name: span.name,
47+
startTime: Number(span.startTime),
48+
endTime: span.endTime ? Number(span.endTime) : undefined,
49+
});
50+
captureException(exit.cause);
51+
});
52+
}
53+
54+
if (originalTracer?.onSpanEnd) {
55+
originalTracer.onSpanEnd(span, exit);
56+
}
57+
},
58+
59+
span<A>(name: string, f: () => A): A {
60+
return startSpan(
61+
{
62+
name,
63+
op: 'effect.span',
64+
origin: 'auto.effect',
65+
},
66+
() => {
67+
try {
68+
return f();
69+
} catch (error) {
70+
const scope = getCurrentScope();
71+
scope.setTag('effect.error', true);
72+
captureException(error);
73+
throw error;
74+
}
75+
},
76+
);
77+
},
78+
79+
withSpan<A>(span: EffectSpan, f: () => A): A {
80+
return startSpan(
81+
{
82+
name: span.name,
83+
op: 'effect.span',
84+
origin: 'auto.effect',
85+
startTime: Number(span.startTime) / 1000000, // Convert nanoseconds to milliseconds
86+
data: span.attributes,
87+
},
88+
sentrySpan => {
89+
try {
90+
const result = f();
91+
92+
// Set status based on span status
93+
if (span.status && span.status.code !== 0) {
94+
sentrySpan.setStatus('internal_error');
95+
if (span.status.message) {
96+
sentrySpan.setData('effect.status_message', span.status.message);
97+
}
98+
}
99+
100+
return result;
101+
} catch (error) {
102+
sentrySpan.setStatus('internal_error');
103+
const scope = getCurrentScope();
104+
scope.setTag('effect.error', true);
105+
captureException(error);
106+
throw error;
107+
}
108+
},
109+
);
110+
},
111+
};
112+
113+
// Register our tracer with Effect
114+
if (typeof Effect.Tracer.set === 'function') {
115+
Effect.Tracer.set(sentryTracer);
116+
} else if (typeof Effect.Tracer.register === 'function') {
117+
Effect.Tracer.register(sentryTracer);
118+
} else if (typeof Effect.Tracer.use === 'function') {
119+
Effect.Tracer.use(sentryTracer);
120+
} else {
121+
return;
122+
}
123+
124+
isInstrumentationEnabled = true;
125+
} catch (error) {
126+
// Silent failure - Effect may not be available
127+
}
128+
},
129+
{ id: INTEGRATION_NAME },
130+
);
131+
132+
const _effectIntegration = (() => {
133+
return {
134+
name: INTEGRATION_NAME,
135+
setupOnce() {
136+
instrumentEffect();
137+
},
138+
};
139+
}) satisfies IntegrationFn;
140+
141+
/**
142+
* Adds Sentry tracing instrumentation for [Effect](https://effect.website/).
143+
*
144+
* This integration automatically traces Effect spans and captures errors that occur
145+
* within Effect computations as Sentry exceptions with proper context.
146+
*
147+
* @example
148+
* ```javascript
149+
* const Sentry = require('@sentry/node');
150+
*
151+
* Sentry.init({
152+
* integrations: [Sentry.effectIntegration()],
153+
* });
154+
* ```
155+
*/
156+
export const effectIntegration = defineIntegration(_effectIntegration);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { effectIntegration, instrumentEffect } from './effectIntegration';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Type definitions based on Effect's actual API
2+
export interface EffectSpan {
3+
name: string;
4+
startTime: bigint;
5+
endTime?: bigint;
6+
attributes?: Record<string, unknown>;
7+
status?: {
8+
code: number;
9+
message?: string;
10+
};
11+
parent?: EffectSpan;
12+
}
13+
14+
export interface EffectExit<E = unknown, A = unknown> {
15+
readonly _tag: 'Success' | 'Failure' | 'Interrupt';
16+
readonly cause?: E;
17+
readonly value?: A;
18+
}
19+
20+
export interface EffectTracer {
21+
onSpanStart?: (span: EffectSpan) => void;
22+
onSpanEnd?: (span: EffectSpan, exit: EffectExit) => void;
23+
span<A>(name: string, f: () => A): A;
24+
withSpan<A>(span: EffectSpan, f: () => A): A;
25+
}
26+
27+
export interface EffectModule {
28+
Tracer?: {
29+
get?: () => EffectTracer | undefined;
30+
current?: () => EffectTracer | undefined;
31+
set?: (tracer: EffectTracer) => void;
32+
register?: (tracer: EffectTracer) => void;
33+
use?: (tracer: EffectTracer) => void;
34+
};
35+
}

packages/node/src/integrations/tracing/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { instrumentOtelHttp, instrumentSentryHttp } from '../http';
33
import { amqplibIntegration, instrumentAmqplib } from './amqplib';
44
import { anthropicAIIntegration, instrumentAnthropicAi } from './anthropic-ai';
55
import { connectIntegration, instrumentConnect } from './connect';
6+
import { effectIntegration, instrumentEffect } from './effect';
67
import { expressIntegration, instrumentExpress } from './express';
78
import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify';
89
import { firebaseIntegration, instrumentFirebase } from './firebase';
@@ -62,6 +63,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
6263
googleGenAIIntegration(),
6364
postgresJsIntegration(),
6465
firebaseIntegration(),
66+
effectIntegration(),
6567
];
6668
}
6769

@@ -101,5 +103,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
101103
instrumentAnthropicAi,
102104
instrumentGoogleGenAI,
103105
instrumentLangGraph,
106+
instrumentEffect,
104107
];
105108
}

0 commit comments

Comments
 (0)