From 3a32838c71b8ec7ef027e622fafbb147d054f982 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 11 May 2026 11:23:49 +0200 Subject: [PATCH] feat(core-shared): Sentry-as-OTel-exporter bridge module --- .../otel/sentry-bridge.test.ts | 34 ++++++++++++++++++ .../src/instrumentation/otel/sentry-bridge.ts | 36 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 packages/core-shared/src/instrumentation/otel/sentry-bridge.test.ts create mode 100644 packages/core-shared/src/instrumentation/otel/sentry-bridge.ts diff --git a/packages/core-shared/src/instrumentation/otel/sentry-bridge.test.ts b/packages/core-shared/src/instrumentation/otel/sentry-bridge.test.ts new file mode 100644 index 0000000..a888062 --- /dev/null +++ b/packages/core-shared/src/instrumentation/otel/sentry-bridge.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +beforeEach(() => vi.resetModules()); + +describe("createSentryOtelBridge", () => { + it("returns a span processor when given a DSN", async () => { + // Mock @sentry/opentelemetry — we only verify the shape of what the bridge returns. + vi.doMock("@sentry/opentelemetry", () => ({ + SentrySpanProcessor: class { + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + }, + })); + const { createSentryOtelBridge } = await import("./sentry-bridge"); + const bridge = createSentryOtelBridge({ dsn: "https://test@sentry.io/1" }); + expect(bridge.spanProcessor).toBeDefined(); + // logRecordProcessor is null in Phase 1 — @sentry/opentelemetry 10.x does not + // export SentryLogRecordProcessor. It will be wired in Phase 3 via @sentry/node. + expect(bridge.logRecordProcessor).toBeNull(); + }); + + it("returns null processors when no DSN provided", async () => { + const { createSentryOtelBridge } = await import("./sentry-bridge"); + const bridge = createSentryOtelBridge({ dsn: "" }); + expect(bridge.spanProcessor).toBeNull(); + expect(bridge.logRecordProcessor).toBeNull(); + }); +}); diff --git a/packages/core-shared/src/instrumentation/otel/sentry-bridge.ts b/packages/core-shared/src/instrumentation/otel/sentry-bridge.ts new file mode 100644 index 0000000..39d888c --- /dev/null +++ b/packages/core-shared/src/instrumentation/otel/sentry-bridge.ts @@ -0,0 +1,36 @@ +import type { SpanProcessor } from "@opentelemetry/sdk-trace-base"; +import type { LogRecordProcessor } from "@opentelemetry/sdk-logs"; + +export type SentryOtelBridgeOpts = { + /** Sentry DSN. When empty, no Sentry processors are returned (Noop boot). */ + dsn: string; +}; + +export type SentryOtelBridge = { + spanProcessor: SpanProcessor | null; + /** + * Log record processor slot. Null in Phase 1 — @sentry/opentelemetry 10.x does + * not export SentryLogRecordProcessor. This slot is filled in Phase 3 when the + * OTel Logger is introduced and wired via @sentry/node's log SDK support. + */ + logRecordProcessor: LogRecordProcessor | null; +}; + +/** + * Creates Sentry-as-OTel-exporter processors. The OTel SDK uses these to + * forward spans (and, in Phase 3, log records) to Sentry. This is the ONLY + * file in core-shared that imports from `@sentry/opentelemetry` — all other + * Sentry coupling is excluded by the R40/R52 ESLint allowlist. + */ +export function createSentryOtelBridge(opts: SentryOtelBridgeOpts): SentryOtelBridge { + if (!opts.dsn) { + return { spanProcessor: null, logRecordProcessor: null }; + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const sentryOtel = require("@sentry/opentelemetry"); + return { + spanProcessor: new sentryOtel.SentrySpanProcessor() as SpanProcessor, + // logRecordProcessor wired in Phase 3 via @sentry/node logger support. + logRecordProcessor: null, + }; +}