diff --git a/apps/backend/middleware/legacy/mirror.middleware.ts b/apps/backend/middleware/legacy/mirror.middleware.ts index f126f1a..2477908 100644 --- a/apps/backend/middleware/legacy/mirror.middleware.ts +++ b/apps/backend/middleware/legacy/mirror.middleware.ts @@ -1,7 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import { MirrorCommandQueue } from './commandqueue.mirror'; - -import { PostHog } from 'posthog-node'; +import { getPostHogClient } from '../../utils/posthog.js'; export const createBackendMirrorMiddleware = (createCommand: (req: Request, data: T) => Promise) => @@ -12,9 +11,7 @@ export const createBackendMirrorMiddleware = res.once('finish', () => { void (async () => { try { - const postHogClient = new PostHog(process.env.POSTHOG_API_KEY ?? '', { - host: 'https://us.i.posthog.com', - }); + const client = getPostHogClient(); console.log('Response finished, checking for mirror work...'); const ok = res.statusCode >= 200 && res.statusCode < 305; @@ -23,7 +20,7 @@ export const createBackendMirrorMiddleware = console.log('Response status:', res.statusCode, 'ok?', ok); const distinctId = (req as any).user?.id ?? req.ip ?? 'anonymous'; - let mirrorOn = await postHogClient.isFeatureEnabled('backend-mirror', distinctId); + let mirrorOn = await client.isFeatureEnabled('backend-mirror', distinctId); mirrorOn ??= false; if (!ok || data == null || !mirrorOn) return; @@ -32,8 +29,6 @@ export const createBackendMirrorMiddleware = const queue = MirrorCommandQueue.instance(); queue.enqueue(await createCommand(req, data)); - - await postHogClient.shutdown(); } catch (e) { console.error('Error in mirror middleware:', e); } diff --git a/apps/backend/utils/posthog.ts b/apps/backend/utils/posthog.ts new file mode 100644 index 0000000..fd05cc4 --- /dev/null +++ b/apps/backend/utils/posthog.ts @@ -0,0 +1,12 @@ +import { PostHog } from 'posthog-node'; + +let postHogClient: PostHog | null = null; + +export function getPostHogClient(): PostHog { + if (!postHogClient) { + postHogClient = new PostHog(process.env.POSTHOG_API_KEY ?? '', { + host: 'https://us.i.posthog.com', + }); + } + return postHogClient; +} diff --git a/tests/unit/middleware/legacy/mirror.posthog.test.ts b/tests/unit/middleware/legacy/mirror.posthog.test.ts new file mode 100644 index 0000000..eceb9b7 --- /dev/null +++ b/tests/unit/middleware/legacy/mirror.posthog.test.ts @@ -0,0 +1,70 @@ +const mockPostHogInstance = { + isFeatureEnabled: jest.fn().mockResolvedValue(true), + shutdown: jest.fn().mockResolvedValue(undefined), +}; + +const mockGetPostHogClient = jest.fn(() => mockPostHogInstance); + +jest.mock('../../../../apps/backend/utils/posthog', () => ({ + getPostHogClient: mockGetPostHogClient, +})); + +jest.mock('../../../../apps/backend/middleware/legacy/commandqueue.mirror', () => ({ + MirrorCommandQueue: { + instance: jest.fn(() => ({ + enqueue: jest.fn(), + })), + }, +})); + +import { createBackendMirrorMiddleware } from '../../../../apps/backend/middleware/legacy/mirror.middleware'; +import { Request, Response } from 'express'; +import { EventEmitter } from 'events'; + +function createMockReqRes() { + const req = { user: { id: 'user-1' }, ip: '127.0.0.1' } as unknown as Request; + + const res = new EventEmitter() as Response & EventEmitter; + res.statusCode = 200; + (res as any).locals = {}; + res.getHeader = jest.fn().mockReturnValue('application/json'); + const origSend = jest.fn().mockReturnThis(); + res.send = origSend; + + return { req, res }; +} + +describe('PostHog client usage', () => { + beforeEach(() => { + mockGetPostHogClient.mockClear(); + mockPostHogInstance.isFeatureEnabled.mockClear(); + mockPostHogInstance.shutdown.mockClear(); + }); + + it('uses the shared PostHog singleton from utils/posthog', async () => { + const createCommand = jest.fn().mockResolvedValue(['SQL1']); + const middleware = createBackendMirrorMiddleware(createCommand); + + const { req: req1, res: res1 } = createMockReqRes(); + const { req: req2, res: res2 } = createMockReqRes(); + const next = jest.fn(); + + await middleware(req1, res1, next); + await middleware(req2, res2, next); + + // Send responses to populate mirrorData + res1.send(JSON.stringify({ ok: true })); + res2.send(JSON.stringify({ ok: true })); + + // Trigger finish events + res1.emit('finish'); + res2.emit('finish'); + + // Allow async callbacks to settle + await new Promise((r) => setTimeout(r, 50)); + + // getPostHogClient is called per-request, but it returns the same singleton + expect(mockGetPostHogClient).toHaveBeenCalled(); + expect(mockPostHogInstance.isFeatureEnabled).toHaveBeenCalledTimes(2); + }); +});