diff --git a/packages/core-shared/package.json b/packages/core-shared/package.json index 6f559d6..8dab678 100644 --- a/packages/core-shared/package.json +++ b/packages/core-shared/package.json @@ -29,6 +29,8 @@ "@repo/core-testing": "workspace:*", "@repo/core-typescript": "workspace:*", "@types/node": "^22.0.0", + "inversify": "^6.2.0", + "reflect-metadata": "^0.2.2", "vitest": "^3.1.0" } } diff --git a/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.test.ts b/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.test.ts new file mode 100644 index 0000000..6d05d86 --- /dev/null +++ b/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.test.ts @@ -0,0 +1,37 @@ +// packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.test.ts +import "reflect-metadata"; +import { describe, it, expect } from "vitest"; +import { Container } from "inversify"; +import { bindNoopInstrumentation } from "@/instrumentation/di/bind-noop-instrumentation"; +import { INSTRUMENTATION_SYMBOLS } from "@/instrumentation/symbols"; +import { NoopTracer } from "@/instrumentation/noop-tracer"; +import { NoopLogger } from "@/instrumentation/noop-logger"; +import type { ITracer, ILogger } from "@/instrumentation"; + +describe("bindNoopInstrumentation", () => { + it("returns a tracer + logger pair", () => { + const c = new Container(); + const { tracer, logger } = bindNoopInstrumentation(c); + expect(tracer).toBeInstanceOf(NoopTracer); + expect(logger).toBeInstanceOf(NoopLogger); + }); + + it("binds TRACER and LOGGER symbols on the container", () => { + const c = new Container(); + bindNoopInstrumentation(c); + const tracer = c.get(INSTRUMENTATION_SYMBOLS.TRACER); + const logger = c.get(INSTRUMENTATION_SYMBOLS.LOGGER); + expect(tracer).toBeInstanceOf(NoopTracer); + expect(logger).toBeInstanceOf(NoopLogger); + }); + + it("is idempotent — second call rebinds the same instances", () => { + const c = new Container(); + const first = bindNoopInstrumentation(c); + const second = bindNoopInstrumentation(c); + // Implementations are NoopX, but instances may differ — that's fine + expect(c.get(INSTRUMENTATION_SYMBOLS.TRACER)).toBe(second.tracer); + expect(c.get(INSTRUMENTATION_SYMBOLS.LOGGER)).toBe(second.logger); + expect(first.tracer).toBeInstanceOf(NoopTracer); + }); +}); diff --git a/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.ts b/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.ts new file mode 100644 index 0000000..73f6695 --- /dev/null +++ b/packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.ts @@ -0,0 +1,23 @@ +// packages/core-shared/src/instrumentation/di/bind-noop-instrumentation.ts +import type { Container } from "inversify"; +import { NoopTracer } from "../noop-tracer"; +import { NoopLogger } from "../noop-logger"; +import { INSTRUMENTATION_SYMBOLS } from "../symbols"; +import type { ITracer, ILogger } from "../index"; + +export function bindNoopInstrumentation(container: Container): { + tracer: ITracer; + logger: ILogger; +} { + const tracer = new NoopTracer(); + const logger = new NoopLogger(); + if (container.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) { + container.unbind(INSTRUMENTATION_SYMBOLS.TRACER); + } + if (container.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) { + container.unbind(INSTRUMENTATION_SYMBOLS.LOGGER); + } + container.bind(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer); + container.bind(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger); + return { tracer, logger }; +} diff --git a/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.test.ts b/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.test.ts new file mode 100644 index 0000000..e381799 --- /dev/null +++ b/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.test.ts @@ -0,0 +1,53 @@ +// packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.test.ts +import "reflect-metadata"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@sentry/nextjs", () => ({ + init: vi.fn(), + startSpan: vi.fn((_opts: unknown, fn: (span: unknown) => unknown) => + fn({ setAttribute: vi.fn(), setStatus: vi.fn() }), + ), + captureException: vi.fn(), + captureMessage: vi.fn(), + addBreadcrumb: vi.fn(), + setUser: vi.fn(), + replayIntegration: vi.fn(() => ({ name: "Replay" })), +})); + +import * as Sentry from "@sentry/nextjs"; +import { Container } from "inversify"; +import { bindSentryInstrumentation } from "@/instrumentation/di/bind-sentry-instrumentation"; +import { INSTRUMENTATION_SYMBOLS } from "@/instrumentation/symbols"; +import { SentryTracer } from "@/instrumentation/sentry/sentry-tracer"; +import { SentryLogger } from "@/instrumentation/sentry/sentry-logger"; + +describe("bindSentryInstrumentation", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls Sentry.init via initSentryServer", () => { + const c = new Container(); + bindSentryInstrumentation(c, { dsn: "https://x@y/1", app: "web-next" }); + expect(Sentry.init).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((Sentry.init as any).mock.calls[0][0].dsn).toBe("https://x@y/1"); + }); + + it("binds SentryTracer + SentryLogger to the container", () => { + const c = new Container(); + bindSentryInstrumentation(c, { dsn: "https://x@y/1", app: "web-next" }); + expect(c.get(INSTRUMENTATION_SYMBOLS.TRACER)).toBeInstanceOf(SentryTracer); + expect(c.get(INSTRUMENTATION_SYMBOLS.LOGGER)).toBeInstanceOf(SentryLogger); + }); + + it("returns the tracer + logger instances", () => { + const c = new Container(); + const { tracer, logger } = bindSentryInstrumentation(c, { + dsn: "https://x@y/1", + app: "web-next", + }); + expect(tracer).toBeInstanceOf(SentryTracer); + expect(logger).toBeInstanceOf(SentryLogger); + }); +}); diff --git a/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.ts b/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.ts new file mode 100644 index 0000000..e52f5b7 --- /dev/null +++ b/packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.ts @@ -0,0 +1,29 @@ +// packages/core-shared/src/instrumentation/di/bind-sentry-instrumentation.ts +import type { Container } from "inversify"; +import { SentryTracer } from "../sentry/sentry-tracer"; +import { SentryLogger } from "../sentry/sentry-logger"; +import { initSentryServer, type InitServerOpts } from "../sentry/init-server"; +import { INSTRUMENTATION_SYMBOLS } from "../symbols"; +import type { ITracer, ILogger } from "../index"; + +export type BindSentryOpts = InitServerOpts; + +export function bindSentryInstrumentation( + container: Container, + opts: BindSentryOpts, +): { tracer: ITracer; logger: ILogger } { + initSentryServer(opts); + + const tracer = new SentryTracer(); + const logger = new SentryLogger(); + + if (container.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) { + container.unbind(INSTRUMENTATION_SYMBOLS.TRACER); + } + if (container.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) { + container.unbind(INSTRUMENTATION_SYMBOLS.LOGGER); + } + container.bind(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer); + container.bind(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger); + return { tracer, logger }; +} diff --git a/packages/core-shared/src/instrumentation/index.ts b/packages/core-shared/src/instrumentation/index.ts index e496a8d..583ba8d 100644 --- a/packages/core-shared/src/instrumentation/index.ts +++ b/packages/core-shared/src/instrumentation/index.ts @@ -13,3 +13,8 @@ export { NoopTracer } from "./noop-tracer"; export { NoopLogger } from "./noop-logger"; export { withSpan } from "./with-span"; export { INSTRUMENTATION_SYMBOLS } from "./symbols"; +export { bindNoopInstrumentation } from "./di/bind-noop-instrumentation"; +export { + bindSentryInstrumentation, + type BindSentryOpts, +} from "./di/bind-sentry-instrumentation"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55ca539..6bb1f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -509,6 +509,12 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.17 + inversify: + specifier: ^6.2.0 + version: 6.2.2(reflect-metadata@0.2.2) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 vitest: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)