Skip to content

Commit 351df12

Browse files
vcarlclaude
andcommitted
Add Effect/OpenTelemetry tracing with Sentry integration
- Upgrade @sentry/node to v8 with skipOpenTelemetrySetup - Add @effect/opentelemetry and OpenTelemetry SDK packages - Add @sentry/opentelemetry for span export - Add posthog-node for future server-side analytics - Create tracing.ts with SentrySpanProcessor, SentrySampler, SentryPropagator - Update runtime.ts to provide TracingLive layer to all effects Effects using Effect.withSpan will now have their spans exported to Sentry for visualization in the Performance dashboard. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d351684 commit 351df12

File tree

5 files changed

+1178
-151
lines changed

5 files changed

+1178
-151
lines changed

app/effects/runtime.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
import { Effect } from "effect";
22

3+
import { TracingLive } from "./tracing.js";
4+
35
/**
4-
* Minimal runtime helpers for running Effects in the Promise-based codebase.
6+
* Runtime helpers for running Effects in the Promise-based codebase.
57
* These provide the bridge between Effect-based code and legacy async/await code.
8+
*
9+
* The tracing layer is automatically provided to all effects run through these
10+
* helpers, so spans created with Effect.withSpan will be exported to Sentry.
611
*/
712

813
/**
914
* Run an Effect and return a Promise that resolves with the success value.
15+
* Automatically provides the tracing layer for Sentry integration.
1016
* Throws if the Effect fails.
1117
*/
1218
export const runEffect = <A, E>(
1319
effect: Effect.Effect<A, E, never>,
14-
): Promise<A> => Effect.runPromise(effect);
20+
): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(TracingLive)));
1521

1622
/**
1723
* Run an Effect and return a Promise that resolves with an Exit value.
24+
* Automatically provides the tracing layer for Sentry integration.
1825
* Never throws - use this when you need to inspect failures.
1926
*/
2027
export const runEffectExit = <A, E>(effect: Effect.Effect<A, E, never>) =>
21-
Effect.runPromiseExit(effect);
28+
Effect.runPromiseExit(effect.pipe(Effect.provide(TracingLive)));
2229

2330
/**
2431
* Run an Effect synchronously.
32+
* Note: Tracing is not provided for sync execution - use runEffect for traced effects.
2533
* Only use for Effects that are guaranteed to be synchronous.
2634
*/
2735
export const runEffectSync = <A, E>(effect: Effect.Effect<A, E, never>): A =>

app/effects/tracing.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NodeSdk } from "@effect/opentelemetry";
2+
import {
3+
SentryPropagator,
4+
SentrySampler,
5+
SentrySpanProcessor,
6+
} from "@sentry/opentelemetry";
7+
8+
import Sentry, { isValidDsn } from "#~/helpers/sentry.server.js";
9+
10+
/**
11+
* Effect OpenTelemetry layer that exports spans to Sentry.
12+
*
13+
* This layer integrates Effect's native tracing (Effect.withSpan) with Sentry.
14+
* All spans created with Effect.withSpan will be exported to Sentry for
15+
* visualization in their Performance dashboard.
16+
*
17+
* The layer uses:
18+
* - SentrySpanProcessor: Exports spans to Sentry (it IS a SpanProcessor, not an exporter)
19+
* - SentrySampler: Respects Sentry's tracesSampleRate
20+
* - SentryPropagator: Enables distributed tracing
21+
*/
22+
export const TracingLive = NodeSdk.layer(() => ({
23+
resource: { serviceName: "mod-bot" },
24+
// Only add Sentry processors if Sentry is configured
25+
// SentrySpanProcessor is already a SpanProcessor, don't wrap in BatchSpanProcessor
26+
spanProcessor: isValidDsn ? new SentrySpanProcessor() : undefined,
27+
sampler: isValidDsn ? new SentrySampler(Sentry.getClient()!) : undefined,
28+
propagator: isValidDsn ? new SentryPropagator() : undefined,
29+
}));

app/helpers/sentry.server.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@ import * as Sentry from "@sentry/node";
33
import { isProd, sentryIngest } from "#~/helpers/env.server";
44

55
// Only initialize Sentry if DSN is valid (not a placeholder like "example.com")
6-
const isValidDsn = sentryIngest.startsWith("https://");
6+
export const isValidDsn = sentryIngest.startsWith("https://");
77

88
if (isValidDsn) {
9-
const sentryOptions = {
9+
const sentryOptions: Sentry.NodeOptions = {
1010
dsn: sentryIngest,
1111
environment: isProd() ? "production" : "development",
12-
integrations: [
13-
new Sentry.Integrations.OnUncaughtException(),
14-
new Sentry.Integrations.OnUnhandledRejection(),
15-
],
16-
12+
// Skip Sentry's auto OpenTelemetry setup - we'll use Effect's OpenTelemetry
13+
// and provide the SentrySpanProcessor to it
14+
skipOpenTelemetrySetup: true,
1715
// Set tracesSampleRate to 1.0 to capture 100%
1816
// of transactions for performance monitoring.
19-
// We recommend adjusting this value in production
2017
tracesSampleRate: isProd() ? 0.2 : 1,
2118
sendDefaultPii: true,
2219
};

0 commit comments

Comments
 (0)