import { beforeEach, describe, expect, it, vi } from "vitest"; const bakedConfig = vi.hoisted(() => ({ enabled: false, posthogApiKey: "", posthogHost: "", sentryDsn: "", sampleRate: 1.0, })); const mockCapture = vi.hoisted(() => vi.fn()); const mockShutdown = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const MockPostHog = vi.hoisted(() => vi.fn().mockImplementation(() => ({ capture: mockCapture, shutdown: mockShutdown, })), ); const mockSentryCapture = vi.hoisted(() => vi.fn()); const mockSentryClose = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const mockSentryInit = vi.hoisted(() => vi.fn()); vi.mock("@snapotter/shared", async (importOriginal) => { const actual: Record = await importOriginal(); return { ...actual, ANALYTICS_BAKED: bakedConfig, }; }); vi.mock("../../../apps/api/src/db/index.js", () => ({ db: { select: () => ({ from: () => ({ where: () => Promise.resolve([]), }), }), }, pool: {}, closeDb: async () => {}, schema: { settings: { key: "key" }, }, })); vi.mock("drizzle-orm", () => ({ eq: () => "mocked-eq", })); vi.mock("posthog-node", () => ({ PostHog: MockPostHog, })); vi.mock("@sentry/node", () => ({ init: mockSentryInit, captureException: mockSentryCapture, close: mockSentryClose, })); type AnalyticsModule = typeof import("../../../apps/api/src/lib/analytics.js"); let mod: AnalyticsModule; beforeEach(async () => { bakedConfig.enabled = false; bakedConfig.posthogApiKey = ""; bakedConfig.posthogHost = ""; bakedConfig.sentryDsn = ""; bakedConfig.sampleRate = 1.0; mockCapture.mockClear(); mockShutdown.mockClear(); MockPostHog.mockClear(); mockSentryCapture.mockClear(); mockSentryClose.mockClear(); mockSentryInit.mockClear(); vi.resetModules(); mod = await import("../../../apps/api/src/lib/analytics.js"); }); describe("initAnalytics", () => { it("does nothing when ANALYTICS_BAKED.enabled is false", async () => { bakedConfig.enabled = false; await expect(mod.initAnalytics()).resolves.toBeUndefined(); expect(MockPostHog).not.toHaveBeenCalled(); }); it("does nothing when enabled is true but posthogApiKey is empty", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = ""; await expect(mod.initAnalytics()).resolves.toBeUndefined(); expect(MockPostHog).not.toHaveBeenCalled(); }); it("initializes posthog when enabled with API key", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.posthogHost = "https://test.posthog.com"; await mod.initAnalytics(); expect(MockPostHog).toHaveBeenCalledWith("phc_test_key", { host: "https://test.posthog.com", flushAt: 20, flushInterval: 30000, }); }); it("does not initialize sentry (moved to preload)", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sentryDsn = "https://test@sentry.io/123"; await mod.initAnalytics(); expect(mockSentryInit).not.toHaveBeenCalled(); }); }); describe("captureException", () => { it("does nothing when sentryModule is null", async () => { await expect(mod.captureException(new Error("test"))).resolves.toBeUndefined(); }); it("captures when sentry is initialized", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sentryDsn = "https://test@sentry.io/123"; await mod.initAnalytics(); const err = new Error("test error"); await mod.captureException(err); expect(mockSentryCapture).toHaveBeenCalledWith(err); }); }); describe("shutdownAnalytics", () => { it("resolves without error when no clients initialized", async () => { await expect(mod.shutdownAnalytics()).resolves.toBeUndefined(); }); it("shuts down posthog when initialized", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; await mod.initAnalytics(); await mod.shutdownAnalytics(); expect(mockShutdown).toHaveBeenCalled(); }); it("does not close sentry (moved to preload)", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sentryDsn = "https://test@sentry.io/123"; await mod.initAnalytics(); await mod.shutdownAnalytics(); expect(mockSentryClose).not.toHaveBeenCalled(); }); }); describe("trackEvent", () => { it("does nothing when posthogClient is null", async () => { await expect(mod.trackEvent("test_event", { key: "value" })).resolves.toBeUndefined(); }); it("does nothing when ANALYTICS_BAKED.enabled is false", async () => { bakedConfig.enabled = false; await expect(mod.trackEvent("test_event", { key: "value" })).resolves.toBeUndefined(); }); it("does nothing when sampleRate is 0", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sampleRate = 0; await mod.initAnalytics(); await mod.trackEvent("test_event", { key: "value" }); expect(mockCapture).not.toHaveBeenCalled(); }); it("captures event when enabled and initialized", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sampleRate = 1.0; await mod.initAnalytics(); await mod.trackEvent("tool_used", { tool: "resize" }); expect(mockCapture).toHaveBeenCalledWith({ distinctId: "unknown", event: "tool_used", properties: { tool: "resize" }, }); }); it("uses provided distinctId when given", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sampleRate = 1.0; await mod.initAnalytics(); await mod.trackEvent("tool_used", { tool: "crop" }, "custom-id-123"); expect(mockCapture).toHaveBeenCalledWith( expect.objectContaining({ distinctId: "custom-id-123", }), ); }); it("does not throw when capture throws internally", async () => { bakedConfig.enabled = true; bakedConfig.posthogApiKey = "phc_test_key"; bakedConfig.sampleRate = 1.0; await mod.initAnalytics(); mockCapture.mockImplementationOnce(() => { throw new Error("capture failed"); }); await expect(mod.trackEvent("test_event", { key: "value" })).resolves.toBeUndefined(); }); });