From 963fd28de392846ff09e88b81945e1b070c8e083 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sun, 10 May 2026 14:38:06 +0200 Subject: [PATCH 1/3] feat(monitoring): single-source PostHog Error Tracking + CLI UA middleware - Remove @sentry/node dep + delete lib/services/sentry.js + sentry tests - Simplify errorTracker.js: PostHog-only fan-out, drop Sentry references - Drop sentryService.init() from app.js bootstrap and shutdown - Drop sentry config from development.config.js + production.config.js - Flip posthog.errorTracking default to true - Replace monitoring/Sentry readiness row with errorTracking/PostHog row - NEW posthog-context.middleware.js: parse @trawlme/cli/ UA header - Wire posthogContextMiddleware in express.js after CORS, before routes - analytics.capture() accepts optional req to merge req.posthogContext - NEW posthog-context.middleware.unit.tests.js (7 cases) - NEW home.service.unit.tests.js (readiness unit tests) - Update errorTracker.unit.tests.js and home.integration.tests.js --- config/defaults/development.config.js | 7 +- config/defaults/production.config.js | 5 - lib/app.js | 3 - lib/middlewares/posthog-context.middleware.js | 33 + .../posthog-context.middleware.unit.tests.js | 84 ++ lib/services/analytics.js | 8 +- lib/services/errorTracker.js | 57 +- lib/services/express.js | 7 +- lib/services/sentry.js | 77 -- lib/services/tests/errorTracker.unit.tests.js | 129 +-- lib/services/tests/sentry.unit.tests.js | 48 - modules/home/services/home.service.js | 10 +- modules/home/tests/home.integration.tests.js | 7 +- modules/home/tests/home.service.unit.tests.js | 117 +++ package-lock.json | 842 +----------------- package.json | 1 - 16 files changed, 285 insertions(+), 1150 deletions(-) create mode 100644 lib/middlewares/posthog-context.middleware.js create mode 100644 lib/middlewares/tests/posthog-context.middleware.unit.tests.js delete mode 100644 lib/services/sentry.js delete mode 100644 lib/services/tests/sentry.unit.tests.js create mode 100644 modules/home/tests/home.service.unit.tests.js diff --git a/config/defaults/development.config.js b/config/defaults/development.config.js index 1c12afa13..5b8b09a20 100644 --- a/config/defaults/development.config.js +++ b/config/defaults/development.config.js @@ -79,11 +79,6 @@ const config = { trust: { proxy: false, }, - sentry: { - dsn: process.env.DEVKIT_NODE_sentry_dsn || '', - environment: process.env.DEVKIT_NODE_sentry_environment || 'development', - enabled: false, - }, posthog: { enabled: false, // set to true + apiKey to activate (default off, no breakage on unconfigured projects) // apiKey: process.env.DEVKIT_NODE_posthog_apiKey ?? '', @@ -91,7 +86,7 @@ const config = { // appTag: process.env.DEVKIT_NODE_posthog_appTag ?? '', // e.g. 'trawl', 'comes' — auto-injected on every capture flushAt: 20, flushInterval: 10000, - errorTracking: false, // opt-in: capture exceptions to PostHog (default: off) + errorTracking: true, // PostHog Error Tracking — active when posthog.apiKey is set autoCapture: false, // opt-in: auto-capture api_request events (default: off) }, domain: '', diff --git a/config/defaults/production.config.js b/config/defaults/production.config.js index 9303d9551..a8eb4a56f 100644 --- a/config/defaults/production.config.js +++ b/config/defaults/production.config.js @@ -55,11 +55,6 @@ const config = { json: true, level: 'info', }, - sentry: { - dsn: process.env.DEVKIT_NODE_sentry_dsn || '', - environment: 'production', - enabled: !!process.env.DEVKIT_NODE_sentry_dsn, - }, }; export default config; diff --git a/lib/app.js b/lib/app.js index 195bcc2c9..20cda0c55 100644 --- a/lib/app.js +++ b/lib/app.js @@ -11,7 +11,6 @@ import express from './services/express.js'; import mongooseService from './services/mongoose.js'; import migrations from './services/migrations.js'; import AnalyticsService from './services/analytics.js'; -import SentryService from './services/sentry.js'; // Establish a MongoDB connection, instantiating all models const startMongoose = async () => { @@ -65,7 +64,6 @@ const bootstrap = async () => { let app; try { - await SentryService.init(); db = await startMongoose(); // DEVKIT_MIGRATIONS_RAN is set by jest.globalSetup.js before any vm context // is created, so it persists across all test suite vm context teardown cycles. @@ -176,7 +174,6 @@ const shutdown = async (server) => { try { const value = await server; await AnalyticsService.shutdown(); - await SentryService.shutdown(); await mongooseService.disconnect(); value.http.close((err) => { if (err) { diff --git a/lib/middlewares/posthog-context.middleware.js b/lib/middlewares/posthog-context.middleware.js new file mode 100644 index 000000000..380be66cf --- /dev/null +++ b/lib/middlewares/posthog-context.middleware.js @@ -0,0 +1,33 @@ +/** + * PostHog context middleware. + * + * Parses the `User-Agent` header to determine the request source and + * attaches a `posthogContext` object to the request for downstream use + * (e.g. enriching analytics events with CLI vs web attribution). + * + * Detection: `@trawlme/cli/` in UA → source: 'cli', cli_version: '' + * Everything else (browser, curl, unknown) → source: 'web' + */ + +const CLI_UA_RE = /@trawlme\/cli\/(\S+)/; + +/** + * Attach PostHog context to every request based on the User-Agent header. + * + * Sets `req.posthogContext` with: + * - `source`: `'cli'` when `@trawlme/cli/` is detected, `'web'` otherwise + * - `cli_version`: CLI version string (only present when source is `'cli'`) + * + * @param {import('express').Request} req - Express request + * @param {import('express').Response} _res - Express response (unused) + * @param {import('express').NextFunction} next - Next middleware + * @returns {void} + */ +export const posthogContextMiddleware = (req, _res, next) => { + const ua = req.get('User-Agent') || ''; + const match = ua.match(CLI_UA_RE); + req.posthogContext = match + ? { source: 'cli', cli_version: match[1] } + : { source: 'web' }; + next(); +}; diff --git a/lib/middlewares/tests/posthog-context.middleware.unit.tests.js b/lib/middlewares/tests/posthog-context.middleware.unit.tests.js new file mode 100644 index 000000000..15fceb7ac --- /dev/null +++ b/lib/middlewares/tests/posthog-context.middleware.unit.tests.js @@ -0,0 +1,84 @@ +/** + * Module dependencies. + */ +import { jest, describe, test, expect, beforeEach } from '@jest/globals'; +import { posthogContextMiddleware } from '../posthog-context.middleware.js'; + +/** + * Unit tests for posthog-context middleware. + * Verifies User-Agent parsing for CLI vs web source attribution: + * 1. CLI UA with version → source:'cli', cli_version:'' + * 2. CLI UA without explicit version segment → source:'cli' fallback + * 3. Web browser UA → source:'web' + * 4. Missing UA → source:'web' + */ +describe('posthogContextMiddleware unit tests:', () => { + let req; + let res; + let next; + + beforeEach(() => { + req = { + get: jest.fn(), + }; + res = {}; + next = jest.fn(); + }); + + test('CLI UA with version → source:cli + cli_version', () => { + req.get.mockReturnValue('@trawlme/cli/1.2.3'); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'cli', cli_version: '1.2.3' }); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('CLI UA with pre-release version → source:cli + cli_version', () => { + req.get.mockReturnValue('@trawlme/cli/2.0.0-beta.1 node/22.0.0'); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'cli', cli_version: '2.0.0-beta.1' }); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('web browser UA → source:web (no cli_version)', () => { + req.get.mockReturnValue('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'web' }); + expect(req.posthogContext).not.toHaveProperty('cli_version'); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('missing User-Agent header → source:web (no cli_version)', () => { + req.get.mockReturnValue(undefined); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'web' }); + expect(req.posthogContext).not.toHaveProperty('cli_version'); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('empty User-Agent header → source:web', () => { + req.get.mockReturnValue(''); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'web' }); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('curl UA → source:web', () => { + req.get.mockReturnValue('curl/8.7.1'); + posthogContextMiddleware(req, res, next); + + expect(req.posthogContext).toEqual({ source: 'web' }); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('always calls next()', () => { + req.get.mockReturnValue('@trawlme/cli/0.1.0'); + posthogContextMiddleware(req, res, next); + + expect(next).toHaveBeenCalledWith(); // called with no args (no error) + }); +}); diff --git a/lib/services/analytics.js b/lib/services/analytics.js index f1b237a5f..10f43a8a7 100644 --- a/lib/services/analytics.js +++ b/lib/services/analytics.js @@ -61,18 +61,24 @@ const track = (distinctId, event, properties, groups) => { * into every event. Custom properties take precedence over defaults. * No-op when client is not initialised, distinctId or event are missing. * + * When `req` is supplied and `req.posthogContext` is set (by posthogContextMiddleware), + * its properties (source, cli_version) are merged into event properties so that + * CLI-originated requests are attributed correctly. + * * @param {Object} params - Event parameters * @param {string} params.distinctId - User or anonymous identifier * @param {string} params.event - Event name * @param {Object} [params.properties] - Additional event properties (win over defaults) + * @param {import('express').Request} [params.req] - Optional Express request for context injection * @returns {void} */ -const capture = ({ distinctId, event, properties = {} } = {}) => { +const capture = ({ distinctId, event, properties = {}, req } = {}) => { if (!client) return; if (!distinctId || !event) return; const defaults = { env: process.env.NODE_ENV || 'development', ...(_appTag ? { app: _appTag } : {}), + ...(req?.posthogContext ?? {}), }; try { client.capture({ distinctId, event, properties: { ...defaults, ...properties } }); diff --git a/lib/services/errorTracker.js b/lib/services/errorTracker.js index 529f78d3e..6f86cc34f 100644 --- a/lib/services/errorTracker.js +++ b/lib/services/errorTracker.js @@ -2,17 +2,15 @@ * Module dependencies */ import config from '../../config/index.js'; -import sentryService from './sentry.js'; import analyticsService from './analytics.js'; /** - * Capture an exception, fanning out to all active trackers. + * Capture an exception in PostHog. * - * - Sentry : active when `config.sentry.dsn` is set (and `enabled !== false`) - * - PostHog : active when `config.posthog.apiKey` is set AND - * `config.posthog.errorTracking === true` + * Active when `config.posthog.apiKey` is set AND + * `config.posthog.errorTracking === true`. * - * Safe no-op when neither tracker is configured. + * Safe no-op when PostHog is not configured. * * @param {Error} err - Error to capture * @param {Object} [ctx] - Optional context attached to the event @@ -21,30 +19,6 @@ import analyticsService from './analytics.js'; * @returns {void} */ const captureException = (err, ctx = {}) => { - // Sentry fan-out - const sentryConfig = config?.sentry ?? {}; - if (sentryConfig.dsn && sentryConfig.enabled !== false) { - sentryService.captureException(err); - } - - // PostHog fan-out — only when errorTracking is explicitly opted-in - const posthogConfig = config?.posthog ?? {}; - if (posthogConfig.apiKey && posthogConfig.errorTracking === true) { - analyticsService.captureException(err, ctx); - } -}; - -/** - * Capture an exception in PostHog only (skips Sentry). - * Used inside the Express error middleware where Sentry's own Express handler - * has already reported the error — calling captureException() here would - * report it to Sentry a second time. - * - * @param {Error} err - Error to capture - * @param {Object} [ctx] - Optional context - * @returns {void} - */ -const captureExceptionPostHogOnly = (err, ctx = {}) => { const posthogConfig = config?.posthog ?? {}; if (posthogConfig.apiKey && posthogConfig.errorTracking === true) { analyticsService.captureException(err, ctx); @@ -52,32 +26,24 @@ const captureExceptionPostHogOnly = (err, ctx = {}) => { }; /** - * Initialise all configured trackers (Sentry + PostHog). - * Safe to call when neither is configured. + * Initialise PostHog analytics (error tracking backend). + * Safe to call when PostHog is not configured. * @returns {Promise} */ const init = async () => { - await Promise.all([ - sentryService.init(), - analyticsService.init(), - ]); + await analyticsService.init(); }; /** - * Set up Express error handling for all active trackers. + * Set up Express error handling for PostHog Error Tracking. * * Must be called after all routes are mounted. - * Mounts Sentry's Express error handler first (captures structured request - * context), then a PostHog-only fan-out middleware to avoid double-reporting - * to Sentry (which is already covered by Sentry's own Express handler). + * Mounts a 4-arg error middleware that captures the exception + * to PostHog and passes it down to the next handler. * * @param {import('express').Express} app - Express application instance */ const setupExpressErrorHandler = (app) => { - // Sentry Express handler (structured request/response context) - sentryService.setupExpressErrorHandler(app); - - // PostHog-only fan-out middleware — Sentry already handled above // `_res` is required for Express to recognise this as a 4-arg error handler app.use((err, req, _res, next) => { const distinctId = req.user?._id @@ -85,7 +51,7 @@ const setupExpressErrorHandler = (app) => { : req.user?.id ? String(req.user.id) : 'anonymous'; - captureExceptionPostHogOnly(err, { distinctId, requestId: req.id }); + captureException(err, { distinctId, requestId: req.id }); next(err); }); }; @@ -93,6 +59,5 @@ const setupExpressErrorHandler = (app) => { export default { init, captureException, - captureExceptionPostHogOnly, setupExpressErrorHandler, }; diff --git a/lib/services/express.js b/lib/services/express.js index 5a3a4e6a8..5886ab128 100644 --- a/lib/services/express.js +++ b/lib/services/express.js @@ -21,6 +21,7 @@ import config from '../../config/index.js'; import guidesHelper from '../helpers/guides.js'; import logger from './logger.js'; import requestId from '../middlewares/requestId.js'; +import { posthogContextMiddleware } from '../middlewares/posthog-context.middleware.js'; import errorTracker from './errorTracker.js'; import AnalyticsService from './analytics.js'; import analyticsMiddleware from '../middlewares/analytics.js'; @@ -300,8 +301,10 @@ const init = async () => { } catch (err) { logger.warn('[analytics] init failed, running without analytics: %s', err.message); } - // Initialize Express middleware + // Initialize Express middleware (includes CORS) initMiddleware(app); + // Attach PostHog context (source: 'cli'|'web') to req after CORS, before routes + app.use(posthogContextMiddleware); // Initialize Helmet security headers initHelmetHeaders(app); // Initialize modules static client routes, @@ -314,7 +317,7 @@ const init = async () => { await initModulesServerPolicies(app); // Initialize modules server routes await initModulesServerRoutes(app); - // Mount error tracker handler (Sentry + fan-out) — must be after routes + // Mount error tracker handler (PostHog Error Tracking) — must be after routes errorTracker.setupExpressErrorHandler(app); // Initialize error routes initErrorRoutes(app); diff --git a/lib/services/sentry.js b/lib/services/sentry.js deleted file mode 100644 index b1b542c86..000000000 --- a/lib/services/sentry.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Module dependencies - */ -import config from '../../config/index.js'; -import logger from './logger.js'; - -/** - * Sentry client reference (null when not configured). - * @type {import('@sentry/node')|null} - */ -let Sentry = null; - -/** - * Initialise Sentry error tracking. - * When `sentry.dsn` is absent or `sentry.enabled` is false the service stays - * in no-op mode — every public method silently returns without side-effects. - * - * The `@sentry/node` SDK is lazy-loaded (dynamic import) so that - * applications that don't use Sentry are never affected. - * @returns {Promise} - */ -const init = async () => { - const { dsn, environment, enabled } = config.sentry ?? {}; - if (!dsn || enabled === false) return; - - try { - const sentryModule = await import('@sentry/node'); - Sentry = sentryModule.default || sentryModule; - Sentry.init({ - dsn, - environment: environment || process.env.NODE_ENV || 'development', - // Adjust sample rates for production - tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, - }); - } catch (err) { - logger.error('Sentry init failed:', err); - Sentry = null; - } -}; - -/** - * Set up the Sentry Express error handler. - * Must be called after all routes are mounted. - * @param {Object} app - Express application instance - */ -const setupExpressErrorHandler = (app) => { - if (!Sentry) return; - Sentry.setupExpressErrorHandler(app); -}; - -/** - * Capture an exception in Sentry. - * Safe to call even when Sentry is not configured. - * @param {Error} err - Error to capture - */ -const captureException = (err) => { - if (!Sentry) return; - Sentry.captureException(err); -}; - -/** - * Flush pending events and close the Sentry client. - * @param {number} [timeout=2000] - Timeout in ms - * @returns {Promise} - */ -const shutdown = async (timeout = 2000) => { - if (!Sentry) return; - await Sentry.close(timeout); - Sentry = null; -}; - -export default { - init, - setupExpressErrorHandler, - captureException, - shutdown, -}; diff --git a/lib/services/tests/errorTracker.unit.tests.js b/lib/services/tests/errorTracker.unit.tests.js index 8626704c8..d5eb5d3f1 100644 --- a/lib/services/tests/errorTracker.unit.tests.js +++ b/lib/services/tests/errorTracker.unit.tests.js @@ -4,36 +4,22 @@ import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; /** - * Unit tests for errorTracker service. - * Tests all 4 combinations: - * 1. No trackers configured → silent no-op - * 2. Sentry only → only Sentry receives exception - * 3. PostHog only (errorTracking=true) → only PostHog receives exception - * 4. Both active → both receive exception - * Also verifies the default-safe invariant: - * - PostHog key alone (no errorTracking) → no exception captured + * Unit tests for errorTracker service (PostHog single-source). + * Tests PostHog-only fan-out — Sentry has been removed. + * Combinations: + * 1. No PostHog configured → silent no-op + * 2. PostHog apiKey set, errorTracking=true → exception captured + * 3. PostHog apiKey set, errorTracking=false → no capture (default-safe) + * 4. PostHog apiKey set, errorTracking missing → no capture (default-safe) */ describe('errorTracker service unit tests:', () => { - let mockSentryCapture; - let mockSentrySetup; let mockAnalyticsCapture; beforeEach(() => { jest.resetModules(); - mockSentryCapture = jest.fn(); - mockSentrySetup = jest.fn(); mockAnalyticsCapture = jest.fn(); - jest.unstable_mockModule('../sentry.js', () => ({ - default: { - init: jest.fn().mockResolvedValue(undefined), - captureException: mockSentryCapture, - setupExpressErrorHandler: mockSentrySetup, - shutdown: jest.fn().mockResolvedValue(undefined), - }, - })); - jest.unstable_mockModule('../analytics.js', () => ({ default: { init: jest.fn().mockResolvedValue(undefined), @@ -53,9 +39,9 @@ describe('errorTracker service unit tests:', () => { }); describe('no trackers configured', () => { - test('should be a silent no-op when neither sentry nor posthog is configured', async () => { + test('should be a silent no-op when posthog is not configured', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { sentry: {}, posthog: {} }, + default: { posthog: {} }, })); const { default: errorTracker } = await import('../errorTracker.js'); @@ -63,7 +49,6 @@ describe('errorTracker service unit tests:', () => { errorTracker.captureException(err, { distinctId: 'user-1' }); - expect(mockSentryCapture).not.toHaveBeenCalled(); expect(mockAnalyticsCapture).not.toHaveBeenCalled(); }); @@ -77,65 +62,14 @@ describe('errorTracker service unit tests:', () => { errorTracker.captureException(err); - expect(mockSentryCapture).not.toHaveBeenCalled(); - expect(mockAnalyticsCapture).not.toHaveBeenCalled(); - }); - }); - - describe('sentry only', () => { - test('should call sentry.captureException and NOT analytics when only sentry.dsn is set', async () => { - jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { - sentry: { dsn: 'https://fake@sentry.io/1', enabled: true }, - posthog: {}, - }, - })); - - const { default: errorTracker } = await import('../errorTracker.js'); - const err = new Error('sentry test'); - - errorTracker.captureException(err, { distinctId: 'user-2', requestId: 'req-abc' }); - - expect(mockSentryCapture).toHaveBeenCalledTimes(1); - expect(mockSentryCapture).toHaveBeenCalledWith(err); - expect(mockAnalyticsCapture).not.toHaveBeenCalled(); - }); - - test('should call sentry even when enabled is not explicitly set (default behavior)', async () => { - jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { - sentry: { dsn: 'https://fake@sentry.io/2' }, - posthog: {}, - }, - })); - - const { default: errorTracker } = await import('../errorTracker.js'); - errorTracker.captureException(new Error('no enabled flag')); - - expect(mockSentryCapture).toHaveBeenCalledTimes(1); expect(mockAnalyticsCapture).not.toHaveBeenCalled(); }); - - test('should NOT call sentry when enabled is false', async () => { - jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { - sentry: { dsn: 'https://fake@sentry.io/3', enabled: false }, - posthog: {}, - }, - })); - - const { default: errorTracker } = await import('../errorTracker.js'); - errorTracker.captureException(new Error('disabled sentry')); - - expect(mockSentryCapture).not.toHaveBeenCalled(); - }); }); describe('posthog only with errorTracking=true', () => { - test('should call analytics.captureException and NOT sentry when posthog is configured with errorTracking=true', async () => { + test('should call analytics.captureException when posthog is configured with errorTracking=true', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: {}, posthog: { apiKey: 'ph_test_key', errorTracking: true }, }, })); @@ -146,7 +80,6 @@ describe('errorTracker service unit tests:', () => { errorTracker.captureException(err, ctx); - expect(mockSentryCapture).not.toHaveBeenCalled(); expect(mockAnalyticsCapture).toHaveBeenCalledTimes(1); expect(mockAnalyticsCapture).toHaveBeenCalledWith(err, ctx); }); @@ -154,7 +87,6 @@ describe('errorTracker service unit tests:', () => { test('should NOT call analytics when posthog.apiKey is set but errorTracking is false (default-safe)', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: {}, posthog: { apiKey: 'ph_test_key', errorTracking: false }, }, })); @@ -162,14 +94,12 @@ describe('errorTracker service unit tests:', () => { const { default: errorTracker } = await import('../errorTracker.js'); errorTracker.captureException(new Error('default-safe test')); - expect(mockSentryCapture).not.toHaveBeenCalled(); expect(mockAnalyticsCapture).not.toHaveBeenCalled(); }); test('should NOT call analytics when posthog.apiKey is set but errorTracking is missing (default-safe)', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: {}, posthog: { apiKey: 'ph_test_key' }, }, })); @@ -177,35 +107,12 @@ describe('errorTracker service unit tests:', () => { const { default: errorTracker } = await import('../errorTracker.js'); errorTracker.captureException(new Error('no errorTracking key')); - expect(mockSentryCapture).not.toHaveBeenCalled(); expect(mockAnalyticsCapture).not.toHaveBeenCalled(); }); }); - describe('both trackers active', () => { - test('should call both sentry and analytics when both are configured', async () => { - jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { - sentry: { dsn: 'https://fake@sentry.io/4', enabled: true }, - posthog: { apiKey: 'ph_test_key', errorTracking: true }, - }, - })); - - const { default: errorTracker } = await import('../errorTracker.js'); - const err = new Error('both trackers test'); - const ctx = { distinctId: 'user-4', requestId: 'req-both' }; - - errorTracker.captureException(err, ctx); - - expect(mockSentryCapture).toHaveBeenCalledTimes(1); - expect(mockSentryCapture).toHaveBeenCalledWith(err); - expect(mockAnalyticsCapture).toHaveBeenCalledTimes(1); - expect(mockAnalyticsCapture).toHaveBeenCalledWith(err, ctx); - }); - }); - describe('init', () => { - test('should call init on both services', async () => { + test('should call init on analytics service', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: {}, })); @@ -213,18 +120,15 @@ describe('errorTracker service unit tests:', () => { const { default: errorTracker } = await import('../errorTracker.js'); await errorTracker.init(); - const { default: sentryService } = await import('../sentry.js'); const { default: analyticsService } = await import('../analytics.js'); - expect(sentryService.init).toHaveBeenCalled(); expect(analyticsService.init).toHaveBeenCalled(); }); }); describe('setupExpressErrorHandler', () => { - test('should call sentry.setupExpressErrorHandler and mount PostHog-only 4-arg middleware', async () => { + test('should mount PostHog 4-arg error middleware', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: { dsn: 'https://fake@sentry.io/5', enabled: true }, posthog: { apiKey: 'ph_test_key', errorTracking: true }, }, })); @@ -234,22 +138,17 @@ describe('errorTracker service unit tests:', () => { errorTracker.setupExpressErrorHandler(mockApp); - expect(mockSentrySetup).toHaveBeenCalledWith(mockApp); - // 4-arg error middleware should be mounted expect(mockApp.use).toHaveBeenCalledTimes(1); const middleware = mockApp.use.mock.calls[0][0]; expect(middleware.length).toBe(4); // 4-arg = error middleware - // Invoke the middleware and verify only PostHog is called (not Sentry again) + // Invoke the middleware and verify PostHog is called const err = new Error('express error'); const req = { user: { _id: 'user-5' }, id: 'req-5' }; const res = {}; const next = jest.fn(); middleware(err, req, res, next); - // Sentry NOT called from middleware (already handled by Sentry Express handler) - expect(mockSentryCapture).not.toHaveBeenCalled(); - // PostHog called with req.user._id as distinctId expect(mockAnalyticsCapture).toHaveBeenCalledWith(err, { distinctId: 'user-5', requestId: 'req-5', @@ -260,7 +159,6 @@ describe('errorTracker service unit tests:', () => { test('should use req.user.id fallback when _id is absent', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: {}, posthog: { apiKey: 'ph_test_key', errorTracking: true }, }, })); @@ -283,7 +181,6 @@ describe('errorTracker service unit tests:', () => { test('should use anonymous when no user is attached', async () => { jest.unstable_mockModule('../../../config/index.js', () => ({ default: { - sentry: {}, posthog: { apiKey: 'ph_test_key', errorTracking: true }, }, })); diff --git a/lib/services/tests/sentry.unit.tests.js b/lib/services/tests/sentry.unit.tests.js deleted file mode 100644 index 1162608d6..000000000 --- a/lib/services/tests/sentry.unit.tests.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Module dependencies. - */ -import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'; - -// Mock config — Sentry disabled by default -jest.unstable_mockModule('../../../config/index.js', () => ({ - default: { - sentry: { dsn: '', environment: 'test', enabled: false }, - }, -})); - -jest.unstable_mockModule('../logger.js', () => ({ - default: { - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), - }, -})); - -describe('SentryService unit tests:', () => { - let SentryService; - - beforeEach(async () => { - const mod = await import('../sentry.js'); - SentryService = mod.default; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('should be a no-op when dsn is empty', async () => { - await SentryService.init(); - // Should not throw - SentryService.captureException(new Error('test')); - SentryService.setupExpressErrorHandler({}); - await SentryService.shutdown(); - }); - - test('should export init, setupExpressErrorHandler, captureException, shutdown', () => { - expect(typeof SentryService.init).toBe('function'); - expect(typeof SentryService.setupExpressErrorHandler).toBe('function'); - expect(typeof SentryService.captureException).toBe('function'); - expect(typeof SentryService.shutdown).toBe('function'); - }); -}); diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 6271405d5..5bd4eda5e 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -170,12 +170,12 @@ const getReadinessStatus = () => { message: posthogConfigured ? 'PostHog configured' : 'PostHog not configured', }); - // monitoring — Sentry - const sentryConfigured = isSet(config.sentry?.dsn); + // errorTracking — PostHog + const errorTrackingEnabled = posthogConfigured && config.posthog?.errorTracking === true; checks.push({ - category: 'monitoring', - status: sentryConfigured ? 'ok' : 'warning', - message: sentryConfigured ? 'Sentry configured' : 'Sentry not configured', + category: 'errorTracking', + status: errorTrackingEnabled ? 'ok' : 'warning', + message: errorTrackingEnabled ? 'PostHog $exception capture enabled' : 'PostHog Error Tracking not enabled (set posthog.errorTracking=true)', }); return checks; diff --git a/modules/home/tests/home.integration.tests.js b/modules/home/tests/home.integration.tests.js index 9e21df963..794c8cf11 100644 --- a/modules/home/tests/home.integration.tests.js +++ b/modules/home/tests/home.integration.tests.js @@ -230,7 +230,7 @@ describe('Home integration tests:', () => { test('should return correct shape for each readiness check', async () => { const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); - const expectedCategories = ['config', 'security', 'auth', 'mail', 'billing', 'analytics', 'monitoring']; + const expectedCategories = ['config', 'security', 'auth', 'mail', 'billing', 'analytics', 'errorTracking']; const categories = result.body.data.map((c) => c.category); expect(categories).toEqual(expectedCategories); result.body.data.forEach((item) => { @@ -249,15 +249,13 @@ describe('Home integration tests:', () => { const origOAuth = config.oAuth; const origStripe = config.stripe; const origPosthog = config.posthog; - const origSentry = config.sentry; const mailerSpy = jest.spyOn(mailer, 'isConfigured').mockReturnValue(true); try { config.domain = 'example.com'; config.jwt.secret = 'a-real-custom-secret-key'; config.oAuth = { google: { clientID: 'google-id' }, apple: { clientID: 'apple-id' } }; config.stripe = { secretKey: 'sk_test_123' }; - config.posthog = { apiKey: 'phk_123' }; - config.sentry = { dsn: 'https://sentry.io/123' }; + config.posthog = { apiKey: 'phk_123', errorTracking: true }; const result = await agent.get('/api/admin/readiness').set('Cookie', `TOKEN=${adminToken}`).expect(200); result.body.data.forEach((item) => { @@ -273,7 +271,6 @@ describe('Home integration tests:', () => { config.oAuth = origOAuth; config.stripe = origStripe; config.posthog = origPosthog; - config.sentry = origSentry; mailerSpy.mockRestore(); } }); diff --git a/modules/home/tests/home.service.unit.tests.js b/modules/home/tests/home.service.unit.tests.js new file mode 100644 index 000000000..dda408ea4 --- /dev/null +++ b/modules/home/tests/home.service.unit.tests.js @@ -0,0 +1,117 @@ +/** + * Module dependencies. + */ +import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; + +/** + * Unit tests for HomeService.getReadinessStatus(). + * Verifies the readiness check rows: drop monitoring/Sentry row, + * add errorTracking/PostHog row. + */ +describe('HomeService.getReadinessStatus unit tests:', () => { + let mockMailerIsConfigured; + + beforeEach(() => { + jest.resetModules(); + mockMailerIsConfigured = jest.fn().mockReturnValue(false); + + // Mock mailer to avoid real SMTP config + jest.unstable_mockModule('../../../lib/helpers/mailer/index.js', () => ({ + default: { isConfigured: mockMailerIsConfigured }, + })); + + // Mock repository to avoid Mongoose schema registration requirement + jest.unstable_mockModule('../repositories/home.repository.js', () => ({ + default: { team: jest.fn().mockResolvedValue([]) }, + })); + + // Mock axios to avoid real network calls + jest.unstable_mockModule('axios', () => ({ + default: { get: jest.fn().mockResolvedValue({ data: [] }), all: jest.fn().mockResolvedValue([]) }, + })); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const withConfig = async (configOverride) => { + jest.unstable_mockModule('../../../config/index.js', () => ({ + default: { + domain: '', + jwt: { secret: 'WaosSecretKeyExampleToChnageAbsolutely' }, + oAuth: {}, + stripe: {}, + posthog: {}, + ...configOverride, + }, + })); + const { default: HomeService } = await import('../services/home.service.js'); + return HomeService; + }; + + test('returns expected category list (no monitoring row, has errorTracking row)', async () => { + const HomeService = await withConfig({}); + const checks = HomeService.getReadinessStatus(); + const categories = checks.map((c) => c.category); + expect(categories).toEqual(['config', 'security', 'auth', 'mail', 'billing', 'analytics', 'errorTracking']); + expect(categories).not.toContain('monitoring'); + }); + + describe('errorTracking row', () => { + test('ok when posthog.apiKey is set AND errorTracking=true', async () => { + const HomeService = await withConfig({ + posthog: { apiKey: 'ph_test_key', errorTracking: true }, + }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'errorTracking'); + expect(row.status).toBe('ok'); + expect(row.message).toBe('PostHog $exception capture enabled'); + }); + + test('warning when posthog.apiKey is set but errorTracking=false', async () => { + const HomeService = await withConfig({ + posthog: { apiKey: 'ph_test_key', errorTracking: false }, + }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'errorTracking'); + expect(row.status).toBe('warning'); + expect(row.message).toContain('posthog.errorTracking=true'); + }); + + test('warning when posthog.apiKey is missing (even if errorTracking=true)', async () => { + const HomeService = await withConfig({ + posthog: { errorTracking: true }, + }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'errorTracking'); + expect(row.status).toBe('warning'); + }); + + test('warning when posthog is not configured at all', async () => { + const HomeService = await withConfig({ posthog: {} }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'errorTracking'); + expect(row.status).toBe('warning'); + }); + }); + + describe('analytics row', () => { + test('ok when posthog.apiKey is set', async () => { + const HomeService = await withConfig({ + posthog: { apiKey: 'ph_test_key' }, + }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'analytics'); + expect(row.status).toBe('ok'); + expect(row.message).toContain('PostHog configured'); + }); + + test('warning when posthog.apiKey is missing', async () => { + const HomeService = await withConfig({ posthog: {} }); + const checks = HomeService.getReadinessStatus(); + const row = checks.find((c) => c.category === 'analytics'); + expect(row.status).toBe('warning'); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 170808148..e6f969a7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@casl/ability": "^6.8.1", "@jest/globals": "^30.3.0", - "@sentry/node": "^10.52.0", "axios": "^1.16.0", "bcrypt": "^6.0.0", "bson": "^7.2.0", @@ -1213,72 +1212,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@fastify/otel": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@fastify/otel/-/otel-0.18.0.tgz", - "integrity": "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.212.0", - "@opentelemetry/semantic-conventions": "^1.28.0", - "minimatch": "^10.2.4" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@fastify/otel/node_modules/@opentelemetry/api-logs": { - "version": "0.212.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz", - "integrity": "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@fastify/otel/node_modules/@opentelemetry/instrumentation": { - "version": "0.212.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", - "integrity": "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.212.0", - "import-in-the-middle": "^2.0.6", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@fastify/otel/node_modules/import-in-the-middle": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", - "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3223,6 +3156,7 @@ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" @@ -3517,6 +3451,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "license": "MIT", + "peer": true, "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -4025,445 +3960,6 @@ "@octokit/openapi-types": "^27.0.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", - "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.214.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.61.0.tgz", - "integrity": "sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.57.0.tgz", - "integrity": "sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.31.0.tgz", - "integrity": "sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.33.0.tgz", - "integrity": "sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.57.0.tgz", - "integrity": "sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.62.0.tgz", - "integrity": "sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.60.0.tgz", - "integrity": "sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.214.0.tgz", - "integrity": "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/instrumentation": "0.214.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz", - "integrity": "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.30.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.58.0.tgz", - "integrity": "sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz", - "integrity": "sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.36.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.58.0.tgz", - "integrity": "sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.67.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.67.0.tgz", - "integrity": "sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz", - "integrity": "sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.60.0.tgz", - "integrity": "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/mysql": "2.15.27" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.60.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.60.0.tgz", - "integrity": "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@opentelemetry/sql-common": "^0.41.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz", - "integrity": "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@opentelemetry/sql-common": "^0.41.2", - "@types/pg": "8.15.6", - "@types/pg-pool": "2.0.7" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz", - "integrity": "sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", - "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", - "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -4555,59 +4051,6 @@ "integrity": "sha512-KuT3vLu3LSFsNWCwasS4gqjH/ysAyIUcB/aJSmKyNhDd/85hAznHRz1eSSl0sMvtsDTYiQIq0I0ybduVbrpPew==", "license": "MIT" }, - "node_modules/@prisma/instrumentation": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.6.0.tgz", - "integrity": "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.207.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.8" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.207.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", - "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { - "version": "0.207.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", - "integrity": "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.207.0", - "import-in-the-middle": "^2.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@prisma/instrumentation/node_modules/import-in-the-middle": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", - "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - } - }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -5208,15 +4651,6 @@ "node": ">=8" } }, - "node_modules/@sentry/core": { - "version": "10.52.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.52.0.tgz", - "integrity": "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@sentry/integrations": { "version": "7.120.4", "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz", @@ -5245,106 +4679,6 @@ "node": ">=8" } }, - "node_modules/@sentry/node": { - "version": "10.52.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.52.0.tgz", - "integrity": "sha512-9+p3KJUk3rHO1HOEZuSknP2RgKCJZONDm4HWgkVDtVBtocb66KLtVlMjc59d2/bWP7tM3wc877tpG30quFfU9g==", - "license": "MIT", - "dependencies": { - "@fastify/otel": "0.18.0", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/core": "^2.6.1", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/instrumentation-amqplib": "0.61.0", - "@opentelemetry/instrumentation-connect": "0.57.0", - "@opentelemetry/instrumentation-dataloader": "0.31.0", - "@opentelemetry/instrumentation-fs": "0.33.0", - "@opentelemetry/instrumentation-generic-pool": "0.57.0", - "@opentelemetry/instrumentation-graphql": "0.62.0", - "@opentelemetry/instrumentation-hapi": "0.60.0", - "@opentelemetry/instrumentation-http": "0.214.0", - "@opentelemetry/instrumentation-kafkajs": "0.23.0", - "@opentelemetry/instrumentation-knex": "0.58.0", - "@opentelemetry/instrumentation-koa": "0.62.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", - "@opentelemetry/instrumentation-mongodb": "0.67.0", - "@opentelemetry/instrumentation-mongoose": "0.60.0", - "@opentelemetry/instrumentation-mysql": "0.60.0", - "@opentelemetry/instrumentation-mysql2": "0.60.0", - "@opentelemetry/instrumentation-pg": "0.66.0", - "@opentelemetry/instrumentation-tedious": "0.33.0", - "@opentelemetry/sdk-trace-base": "^2.6.1", - "@opentelemetry/semantic-conventions": "^1.40.0", - "@prisma/instrumentation": "7.6.0", - "@sentry/core": "10.52.0", - "@sentry/node-core": "10.52.0", - "@sentry/opentelemetry": "10.52.0", - "import-in-the-middle": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/node-core": { - "version": "10.52.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.52.0.tgz", - "integrity": "sha512-IG7MBtLRPQ2LuU+kbD14AFZroZgAeUmJQTP1FI/F8n56O31+p+9R703LuBTpvZr6sm+eRYDMWcGYYkfLHRVjwg==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.52.0", - "@sentry/opentelemetry": "10.52.0", - "import-in-the-middle": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.39.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@opentelemetry/core": { - "optional": true - }, - "@opentelemetry/exporter-trace-otlp-http": { - "optional": true - }, - "@opentelemetry/instrumentation": { - "optional": true - }, - "@opentelemetry/sdk-trace-base": { - "optional": true - }, - "@opentelemetry/semantic-conventions": { - "optional": true - } - } - }, - "node_modules/@sentry/opentelemetry": { - "version": "10.52.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.52.0.tgz", - "integrity": "sha512-Sc7StsvC0bwhMcgDfTRWUIexO5cNzzKUurvUwtpgQUnxO7AzexU3lkY3yHYDsCbWYAEQMXAgQYQtbcqoh+Ie7g==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.52.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.39.0" - } - }, "node_modules/@sentry/types": { "version": "7.120.4", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz", @@ -5513,15 +4847,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -5578,15 +4903,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mysql": { - "version": "2.15.27", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", - "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", @@ -5603,41 +4919,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/pg-pool": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", - "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", - "license": "MIT", - "dependencies": { - "@types/pg": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -5997,6 +5284,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6005,15 +5293,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9262,12 +8541,6 @@ "node": ">= 0.6" } }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -9982,21 +9255,6 @@ "node": ">=18.20" } }, - "node_modules/import-in-the-middle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.0.tgz", - "integrity": "sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -12694,6 +11952,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "license": "MIT", + "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -14476,12 +13735,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/mongodb": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", @@ -17503,37 +16756,6 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17728,45 +16950,6 @@ "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", "license": "MIT-0" }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/posthog-node": { "version": "5.33.4", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.33.4.tgz", @@ -17987,7 +17170,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-is-18": { "name": "react-is", @@ -18170,19 +17354,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" - }, - "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" - } - }, "node_modules/resend": { "version": "6.12.3", "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.3.tgz", @@ -20811,6 +19982,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/package.json b/package.json index abf0bb5e4..daa3e72fc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "dependencies": { "@casl/ability": "^6.8.1", "@jest/globals": "^30.3.0", - "@sentry/node": "^10.52.0", "axios": "^1.16.0", "bcrypt": "^6.0.0", "bson": "^7.2.0", From c938ec6b561539be8e98b3de2937a6f5e5c10906 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sun, 10 May 2026 15:04:52 +0200 Subject: [PATCH 2/3] chore(config): drop leftover sentry block from test.config.js DeepSeek pre-merge audit P1 finding: development.config.js + production.config.js Sentry blocks were dropped, but test.config.js was missed. No functional break (nothing reads it post-removal), but stale config invites confusion. --- config/defaults/test.config.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/defaults/test.config.js b/config/defaults/test.config.js index 73196e69d..9078ee47f 100644 --- a/config/defaults/test.config.js +++ b/config/defaults/test.config.js @@ -26,10 +26,6 @@ const config = { enabled: true, ttlDays: 1, }, - sentry: { - dsn: '', - enabled: false, - }, organizations: { enabled: false, domainMatching: false, From 7c9ca4199345eb13840c920a2a4c634b9bcfc3c0 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Sun, 10 May 2026 15:14:08 +0200 Subject: [PATCH 3/3] docs(migrations) + test(analytics): address P2 audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepSeek pre-merge audit P2 findings: - MIGRATIONS.md: add v10 "Sentry removed" section so /update-project sub-agents understand the propagation expectations (env var drops, @sentry/* dep cleanup, config override removal, posthog.errorTracking opt-in semantics). - analytics.capture.unit.tests.js: add 3 tests covering the new req.posthogContext injection path (merges into defaults, user properties win on conflict, absent req is backward-compat — no source/cli_version leak). --- MIGRATIONS.md | 29 +++++++++++++++ .../tests/analytics.capture.unit.tests.js | 37 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index b27cd856f..1817ebcb9 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -4,6 +4,35 @@ Breaking changes and upgrade notes for downstream projects. --- +## Sentry removed — PostHog Error Tracking is now sole source (2026-05-10) + +The `@sentry/node` integration shipped in 2026-03-26 (still documented below as **PostHog Analytics (2026-03-26)** + the now-removed Sentry monitoring section) is dropped. Error capture moves entirely to PostHog Error Tracking via `posthog.capture('$exception', ...)`. + +### What changed + +- **Deleted** : `lib/services/sentry.js` + its unit tests + the `@sentry/node` dependency. +- **`lib/services/errorTracker.js`** simplified to PostHog-only path. The `captureExceptionPostHogOnly` fan-out helper is removed (collapsed into `captureException` since there is no longer a double-reporting risk from a parallel Sentry Express handler). +- **`lib/app.js`** — Sentry init/shutdown calls removed from bootstrap and shutdown paths. +- **`config/defaults/{development,production,test}.config.js`** — `sentry: { ... }` blocks deleted. +- **`config/defaults/development.config.js` + `production.config.js`** — `posthog.errorTracking` default flipped from `false` → `true`. Error capture is now enabled by default whenever `posthog.apiKey` is set. +- **`modules/home/services/home.service.js`** `getReadinessStatus()` — the `monitoring` row (Sentry presence) is replaced by an `errorTracking` row that gates on `posthog.apiKey && posthog.errorTracking === true`. +- **NEW `lib/middlewares/posthog-context.middleware.js`** — parses the `User-Agent` header, attaches `req.posthogContext = { source: 'cli'|'web', cli_version? }` for CLI-source attribution. Wired in `lib/services/express.js` after CORS / before routes. +- **`lib/services/analytics.js`** `capture()` accepts an optional `req` param. When provided, `req.posthogContext` is merged into event defaults so that CLI-originated requests carry `source` + `cli_version` automatically. Backward-compatible: callers that omit `req` see no behaviour change. + +### Action required for downstream projects (`/update-project`) + +1. **Drop env vars** `SENTRY_DSN` + any `SENTRY_*` references from `.env`, K8s manifests (`clusters/*/apps/*-node.yaml`), `.env.example`, deploy scripts, and CI secrets — they are no longer read. +2. **Drop `@sentry/*` deps** from project `package.json` if pinned downstream. Run `npm install` to regen lockfile. +3. **Remove project `config/defaults/*.config.js` overrides** of the `sentry: { ... }` block — they were either referencing the now-removed config path (no-op merge) or overriding fields that no longer exist. +4. **Confirm `posthog.errorTracking`** : if downstream config explicitly sets `posthog.errorTracking: false` to suppress capture, that override still wins via deepmerge. To opt into error tracking, set it to `true` (or rely on the new default if you remove the override). +5. **Optional — wire `req` into existing `capture()` callers** : if you want CLI-source attribution on existing events, change `capture({ distinctId, event, properties })` → `capture({ distinctId, event, properties, req })`. Without this opt-in, events still capture correctly but lack the `source`/`cli_version` properties. + +### Why + +Cf `infra/docs/superpowers/plans/2026-05-10-posthog-observability-followups.md` (decision matrix). PostHog Error Tracking is GA, free tier covers 100k exceptions/mo, and the single-tracker setup eliminates dual-config drift + cross-tool funnel friction. + +--- + ## Test DB isolation: per-pid Mongo database default + globalTeardown (2026-04-24) Default test database is now `mongodb://127.0.0.1:27017/NodeTest_${process.pid}` instead of the shared `NodeTest`. Concurrent jest invocations (e.g. multiple agent worktrees running `npm run test:coverage` in parallel) get isolated databases, eliminating the 401 / 404 / 422 / `MongoPoolClosedError` flake patterns documented in trawl_node#980. diff --git a/lib/services/tests/analytics.capture.unit.tests.js b/lib/services/tests/analytics.capture.unit.tests.js index 14d6febeb..33f5f17e6 100644 --- a/lib/services/tests/analytics.capture.unit.tests.js +++ b/lib/services/tests/analytics.capture.unit.tests.js @@ -221,6 +221,43 @@ describe('Analytics capture() and enabled-flag:', () => { expect(call.properties).not.toHaveProperty('app'); expect(call.properties).toHaveProperty('env'); }); + + test('merges req.posthogContext (source + cli_version) into event properties', async () => { + AnalyticsService.capture({ + distinctId: 'user-1', + event: 'my_event', + req: { posthogContext: { source: 'cli', cli_version: '1.2.3' } }, + }); + + expect(mockPostHogInstance.capture).toHaveBeenCalledWith({ + distinctId: 'user-1', + event: 'my_event', + properties: expect.objectContaining({ source: 'cli', cli_version: '1.2.3', app: 'myapp' }), + }); + }); + + test('user-supplied properties win over req.posthogContext (precedence)', async () => { + AnalyticsService.capture({ + distinctId: 'user-1', + event: 'my_event', + properties: { source: 'override' }, + req: { posthogContext: { source: 'cli', cli_version: '1.2.3' } }, + }); + + expect(mockPostHogInstance.capture).toHaveBeenCalledWith({ + distinctId: 'user-1', + event: 'my_event', + properties: expect.objectContaining({ source: 'override', cli_version: '1.2.3' }), + }); + }); + + test('absent req does not inject source/cli_version (backward compat)', async () => { + AnalyticsService.capture({ distinctId: 'user-1', event: 'my_event' }); + + const call = mockPostHogInstance.capture.mock.calls[0][0]; + expect(call.properties).not.toHaveProperty('source'); + expect(call.properties).not.toHaveProperty('cli_version'); + }); }); // ─────────────────────────────────────────────────────────────────