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
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export { anthropicAIIntegration } from './integrations/tracing/anthropic-ai';
export { googleGenAIIntegration } from './integrations/tracing/google-genai';
export { langChainIntegration } from './integrations/tracing/langchain';
export { langGraphIntegration } from './integrations/tracing/langgraph';
export { effectIntegration } from './integrations/tracing/effect';
export {
launchDarklyIntegration,
buildLaunchDarklyFlagUsedHandler,
Expand Down
156 changes: 156 additions & 0 deletions packages/node/src/integrations/tracing/effect/effectIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type { IntegrationFn } from '@sentry/core';
import { captureException, defineIntegration, getCurrentScope, startSpan, withScope } from '@sentry/core';
import type { EffectExit, EffectModule, EffectSpan, EffectTracer } from './types';

const INTEGRATION_NAME = 'Effect';

// Global state to track if tracing is enabled
let isInstrumentationEnabled = false;
let originalTracer: EffectTracer | undefined;

/**
* Instruments Effect spans to create Sentry spans.
*/
export const instrumentEffect = Object.assign(
(): void => {
if (isInstrumentationEnabled) {
return;
}

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Effect = require('effect') as EffectModule;

if (!Effect?.Tracer) {
return;
}

// Store the original tracer if it exists
originalTracer = Effect.Tracer.get?.() || Effect.Tracer.current?.() || undefined;

// Create our custom tracer that wraps operations in Sentry spans
const sentryTracer: EffectTracer = {
onSpanStart(span: EffectSpan) {
// Hook for span start - can be used for additional instrumentation
if (originalTracer?.onSpanStart) {
originalTracer.onSpanStart(span);
}
},

onSpanEnd(span: EffectSpan, exit: EffectExit) {
// Hook for span end - handle failures
if (exit._tag === 'Failure' && exit.cause) {
withScope(scope => {
scope.setTag('effect.exit_tag', exit._tag);
scope.setContext('effect.span', {
name: span.name,
startTime: Number(span.startTime),
endTime: span.endTime ? Number(span.endTime) : undefined,
});
captureException(exit.cause);
Copy link

Choose a reason for hiding this comment

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

Missing mechanism parameter in captureException calls

Medium Severity

The captureException calls at lines 50, 72, and 105 are missing the mechanism parameter with handled and type properties. Other integrations like connect.ts, express.ts, and fastify/index.ts properly include { mechanism: { handled: false, type: 'auto.middleware.connect' } }. The mechanism should use a type like 'auto.effect' to align with the span origin.

Additional Locations (2)

Fix in Cursor Fix in Web

});
}

if (originalTracer?.onSpanEnd) {
originalTracer.onSpanEnd(span, exit);
}
},

span<A>(name: string, f: () => A): A {
return startSpan(
{
name,
op: 'effect.span',
origin: 'auto.effect',
Copy link

Choose a reason for hiding this comment

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

Span origin ignored because passed as wrong property

Medium Severity

The origin: 'auto.effect' property is passed directly on the startSpan options object, but StartSpanOptions doesn't have an origin property. The origin must be set via attributes using SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN. All Effect spans will incorrectly have origin 'manual' instead of 'auto.effect'.

Additional Locations (1)

Fix in Cursor Fix in Web

},
() => {
try {
return f();
} catch (error) {
const scope = getCurrentScope();
scope.setTag('effect.error', true);
captureException(error);
throw error;
}
},
);
},

withSpan<A>(span: EffectSpan, f: () => A): A {
return startSpan(
{
name: span.name,
op: 'effect.span',
origin: 'auto.effect',
startTime: Number(span.startTime) / 1000000, // Convert nanoseconds to milliseconds
Copy link

Choose a reason for hiding this comment

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

Bug: The Effect integration incorrectly converts span timestamps from nanoseconds to milliseconds instead of seconds, causing all reported span timings to be off by a factor of 1000.
Severity: HIGH

Suggested Fix

Update the timestamp conversion logic to divide the nanosecond value by 1_000_000_000 to correctly convert it to seconds, which is the unit expected by the Sentry startSpan function. The line should be changed to startTime: Number(span.startTime) / 1_000_000_000.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/node/src/integrations/tracing/effect/effectIntegration.ts#L85

Potential issue: The Effect integration converts span start times from nanoseconds to
milliseconds by dividing the `BigInt` timestamp by `1,000,000`. However, the Sentry
`startSpan` function expects a numeric timestamp to be in seconds. As a result, a
timestamp representing 1 second (`1000` milliseconds) will be interpreted by Sentry as
1000 seconds. This will cause all span start times and durations to be reported
incorrectly by a factor of 1000, making the tracing data unusable.

Did we get this right? 👍 / 👎 to inform future reviews.

data: span.attributes,
Copy link

Choose a reason for hiding this comment

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

Span attributes silently ignored due to wrong property name

Medium Severity

The startSpan call passes data: span.attributes but StartSpanOptions expects attributes, not data. This means the Effect span's attributes will be silently dropped and not transferred to the Sentry span, resulting in loss of trace context.

Fix in Cursor Fix in Web

},
sentrySpan => {
try {
const result = f();

// Set status based on span status
if (span.status && span.status.code !== 0) {
sentrySpan.setStatus('internal_error');
Copy link

Choose a reason for hiding this comment

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

setStatus called with incorrect argument type

High Severity

The setStatus calls pass a plain string 'internal_error' but Span.setStatus() expects a SpanStatus object with code (number: 0, 1, or 2) and optional message properties. Other integrations in the codebase correctly use span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }). This will cause the span status to not be set correctly.

Additional Locations (1)

Fix in Cursor Fix in Web

if (span.status.message) {
sentrySpan.setData('effect.status_message', span.status.message);
Copy link

Choose a reason for hiding this comment

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

setData method does not exist on Span

High Severity

The call to sentrySpan.setData('effect.status_message', span.status.message) uses a method that doesn't exist on the Span interface. The correct method is setAttribute. This will cause a runtime error when this code path is executed.

Fix in Cursor Fix in Web

}
}

return result;
} catch (error) {
sentrySpan.setStatus('internal_error');
const scope = getCurrentScope();
scope.setTag('effect.error', true);
captureException(error);
throw error;
}
},
);
},
};

// Register our tracer with Effect
if (typeof Effect.Tracer.set === 'function') {
Effect.Tracer.set(sentryTracer);
} else if (typeof Effect.Tracer.register === 'function') {
Effect.Tracer.register(sentryTracer);
} else if (typeof Effect.Tracer.use === 'function') {
Effect.Tracer.use(sentryTracer);
} else {
return;
}
Comment on lines +114 to +122

Choose a reason for hiding this comment

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

Where did you find these methods on the effect tracer? I do not think they exist.


isInstrumentationEnabled = true;
} catch (error) {
// Silent failure - Effect may not be available
}
},
{ id: INTEGRATION_NAME },
);

const _effectIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentEffect();
},
};
}) satisfies IntegrationFn;

/**
* Adds Sentry tracing instrumentation for [Effect](https://effect.website/).
*
* This integration automatically traces Effect spans and captures errors that occur
* within Effect computations as Sentry exceptions with proper context.
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
*
* Sentry.init({
* integrations: [Sentry.effectIntegration()],
* });
* ```
*/
export const effectIntegration = defineIntegration(_effectIntegration);
1 change: 1 addition & 0 deletions packages/node/src/integrations/tracing/effect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { effectIntegration, instrumentEffect } from './effectIntegration';
35 changes: 35 additions & 0 deletions packages/node/src/integrations/tracing/effect/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Type definitions based on Effect's actual API
export interface EffectSpan {
name: string;
startTime: bigint;
endTime?: bigint;
attributes?: Record<string, unknown>;
status?: {
code: number;
message?: string;
};
parent?: EffectSpan;
}

export interface EffectExit<E = unknown, A = unknown> {
readonly _tag: 'Success' | 'Failure' | 'Interrupt';
readonly cause?: E;
readonly value?: A;
}

export interface EffectTracer {
onSpanStart?: (span: EffectSpan) => void;
onSpanEnd?: (span: EffectSpan, exit: EffectExit) => void;
Comment on lines +21 to +22

Choose a reason for hiding this comment

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

Where can you find this in effect?

span<A>(name: string, f: () => A): A;

Choose a reason for hiding this comment

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

The span method on custom tracers takes 6 args, not 2.

withSpan<A>(span: EffectSpan, f: () => A): A;

Choose a reason for hiding this comment

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

Where can you find this in the effect tracer interface?

Copy link

Choose a reason for hiding this comment

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

Effect tracer interface method signatures are incorrect

High Severity

The EffectTracer type definitions don't match Effect's actual tracer API. PR reviewer @marbemac noted that the span method takes 6 arguments, not 2, and the withSpan method doesn't exist in Effect's tracer interface. The implementation in effectIntegration.ts uses these incorrect signatures, meaning the integration won't work correctly with the actual Effect library.

Additional Locations (1)

Fix in Cursor Fix in Web

}

export interface EffectModule {
Tracer?: {
get?: () => EffectTracer | undefined;
current?: () => EffectTracer | undefined;
set?: (tracer: EffectTracer) => void;
register?: (tracer: EffectTracer) => void;
use?: (tracer: EffectTracer) => void;
};
}
3 changes: 3 additions & 0 deletions packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { instrumentOtelHttp, instrumentSentryHttp } from '../http';
import { amqplibIntegration, instrumentAmqplib } from './amqplib';
import { anthropicAIIntegration, instrumentAnthropicAi } from './anthropic-ai';
import { connectIntegration, instrumentConnect } from './connect';
import { effectIntegration, instrumentEffect } from './effect';
import { expressIntegration, instrumentExpress } from './express';
import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify';
import { firebaseIntegration, instrumentFirebase } from './firebase';
Expand Down Expand Up @@ -62,6 +63,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
googleGenAIIntegration(),
postgresJsIntegration(),
firebaseIntegration(),
effectIntegration(),
];
}

Expand Down Expand Up @@ -101,5 +103,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentAnthropicAi,
instrumentGoogleGenAI,
instrumentLangGraph,
instrumentEffect,
];
}
Loading
Loading