From e7207a547794dbd2fae3199f391a6502b4d9eae8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 17:24:32 +0000 Subject: [PATCH 01/12] fix(api): enforce bridge signature validation Co-authored-by: Dima Grossman --- .../src/app/environments-v1/novu-bridge-client.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/environments-v1/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts index 39f8eb3aa7a..82f18f7377d 100644 --- a/apps/api/src/app/environments-v1/novu-bridge-client.ts +++ b/apps/api/src/app/environments-v1/novu-bridge-client.ts @@ -1,4 +1,5 @@ import { Inject } from '@nestjs/common'; +import { GetDecryptedSecretKey, GetDecryptedSecretKeyCommand } from '@novu/application-generic'; import { PostActionEnum, type Workflow } from '@novu/framework/internal'; import { Client, NovuHandler, NovuRequestHandler } from '@novu/framework/nest'; import { EnvironmentTypeEnum } from '@novu/shared'; @@ -18,7 +19,8 @@ export const frameworkName = 'novu-nest'; export class NovuBridgeClient { constructor( @Inject(NovuHandler) private novuHandler: NovuHandler, - private constructFrameworkWorkflow: ConstructFrameworkWorkflow + private constructFrameworkWorkflow: ConstructFrameworkWorkflow, + private getDecryptedSecretKey: GetDecryptedSecretKey ) {} public async handleRequest(req: Request, res: Response) { @@ -46,10 +48,16 @@ export class NovuBridgeClient { workflows.push(programmaticallyConstructedWorkflow); } + const secretKey = await this.getDecryptedSecretKey.execute( + GetDecryptedSecretKeyCommand.create({ + environmentId: req.params.environmentId, + }) + ); + const novuRequestHandler = new NovuRequestHandler({ frameworkName, workflows, - client: new Client({ secretKey: 'INTERNAL_KEY', strictAuthentication: false, verbose: false }), + client: new Client({ secretKey, strictAuthentication: true, verbose: false }), handler: this.novuHandler.handler, }); From 1dff313a8a72c48aade72e0174f6195175558d25 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 17:30:57 +0000 Subject: [PATCH 02/12] refactor(api): cache bridge secret key lookup Co-authored-by: Dima Grossman --- .../app/environments-v1/novu-bridge-client.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/api/src/app/environments-v1/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts index 82f18f7377d..cb6f8f60a57 100644 --- a/apps/api/src/app/environments-v1/novu-bridge-client.ts +++ b/apps/api/src/app/environments-v1/novu-bridge-client.ts @@ -1,5 +1,10 @@ import { Inject } from '@nestjs/common'; -import { GetDecryptedSecretKey, GetDecryptedSecretKeyCommand } from '@novu/application-generic'; +import { + GetDecryptedSecretKey, + GetDecryptedSecretKeyCommand, + InMemoryLRUCacheService, + InMemoryLRUCacheStore, +} from '@novu/application-generic'; import { PostActionEnum, type Workflow } from '@novu/framework/internal'; import { Client, NovuHandler, NovuRequestHandler } from '@novu/framework/nest'; import { EnvironmentTypeEnum } from '@novu/shared'; @@ -20,7 +25,8 @@ export class NovuBridgeClient { constructor( @Inject(NovuHandler) private novuHandler: NovuHandler, private constructFrameworkWorkflow: ConstructFrameworkWorkflow, - private getDecryptedSecretKey: GetDecryptedSecretKey + private getDecryptedSecretKey: GetDecryptedSecretKey, + private inMemoryLRUCacheService: InMemoryLRUCacheService ) {} public async handleRequest(req: Request, res: Response) { @@ -48,11 +54,21 @@ export class NovuBridgeClient { workflows.push(programmaticallyConstructedWorkflow); } - const secretKey = await this.getDecryptedSecretKey.execute( - GetDecryptedSecretKeyCommand.create({ - environmentId: req.params.environmentId, - }) - ); + const environmentId = req.params.environmentId; + const secretKey = (await this.inMemoryLRUCacheService.get( + InMemoryLRUCacheStore.VALIDATOR, + `bridge-secret-key:${environmentId}`, + () => + this.getDecryptedSecretKey.execute( + GetDecryptedSecretKeyCommand.create({ + environmentId, + }) + ), + { + environmentId, + cacheVariant: 'bridge-secret-key', + } + )) as string; const novuRequestHandler = new NovuRequestHandler({ frameworkName, From 3f173d241d953be67a96b993eb41bbdb2ba0da44 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 19:00:32 +0000 Subject: [PATCH 03/12] fix(api): validate bridge environmentId and secret key resolution - Reject missing/blank environmentId with 400 before workflow or cache work - Resolve secret without unsafe cast; treat empty result as 500 with context - Map NotFoundException from GetDecryptedSecretKey to 404; log other errors with InMemoryLRUCacheStore.VALIDATOR and bridge-secret-key cache key for debugging Co-authored-by: Dima Grossman --- .../app/environments-v1/novu-bridge-client.ts | 86 +++++++++++++++---- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/apps/api/src/app/environments-v1/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts index cb6f8f60a57..4228190448e 100644 --- a/apps/api/src/app/environments-v1/novu-bridge-client.ts +++ b/apps/api/src/app/environments-v1/novu-bridge-client.ts @@ -1,4 +1,4 @@ -import { Inject } from '@nestjs/common'; +import { Inject, Logger, NotFoundException } from '@nestjs/common'; import { GetDecryptedSecretKey, GetDecryptedSecretKeyCommand, @@ -22,6 +22,8 @@ export const frameworkName = 'novu-nest'; * workflows to serve on the Novu Bridge. */ export class NovuBridgeClient { + private readonly logger = new Logger(NovuBridgeClient.name); + constructor( @Inject(NovuHandler) private novuHandler: NovuHandler, private constructFrameworkWorkflow: ConstructFrameworkWorkflow, @@ -30,6 +32,16 @@ export class NovuBridgeClient { ) {} public async handleRequest(req: Request, res: Response) { + const environmentId = req.params.environmentId; + if (!environmentId || !String(environmentId).trim()) { + res.status(400).json({ + error: 'Missing or invalid environmentId', + details: 'The bridge route requires a non-empty environmentId path parameter.', + }); + + return; + } + const workflows: Workflow[] = []; /* @@ -40,7 +52,7 @@ export class NovuBridgeClient { if (Object.values(PostActionEnum).includes(req.query.action as PostActionEnum)) { const programmaticallyConstructedWorkflow = await this.constructFrameworkWorkflow.execute( ConstructFrameworkWorkflowCommand.create({ - environmentId: req.params.environmentId, + environmentId, workflowId: req.query.workflowId as string, layoutId: req.query.layoutId as string, controlValues: req.body.controls, @@ -54,21 +66,63 @@ export class NovuBridgeClient { workflows.push(programmaticallyConstructedWorkflow); } - const environmentId = req.params.environmentId; - const secretKey = (await this.inMemoryLRUCacheService.get( - InMemoryLRUCacheStore.VALIDATOR, - `bridge-secret-key:${environmentId}`, - () => - this.getDecryptedSecretKey.execute( - GetDecryptedSecretKeyCommand.create({ - environmentId, - }) - ), - { - environmentId, - cacheVariant: 'bridge-secret-key', + const cacheKey = `bridge-secret-key:${environmentId}`; + const storeName = InMemoryLRUCacheStore.VALIDATOR; + + let secretKey: string; + try { + const resolved = await this.inMemoryLRUCacheService.get( + storeName, + cacheKey, + () => + this.getDecryptedSecretKey.execute( + GetDecryptedSecretKeyCommand.create({ + environmentId, + }) + ), + { + environmentId, + cacheVariant: 'bridge-secret-key', + } + ); + + if (typeof resolved !== 'string' || !resolved.trim()) { + this.logger.error( + `Bridge secret key missing or invalid after cache lookup (store=${storeName}, cacheKey=${cacheKey}, environmentId=${environmentId})` + ); + res.status(500).json({ + error: 'Failed to resolve environment secret key', + details: `Empty or invalid secret from ${storeName} for cache key ${cacheKey}.`, + }); + + return; + } + + secretKey = resolved; + } catch (error) { + if (error instanceof NotFoundException) { + this.logger.warn( + `Environment not found for bridge secret (store=${storeName}, cacheKey=${cacheKey}): ${error.message}` + ); + res.status(404).json({ + error: 'Environment not found', + details: `No environment for cache key ${cacheKey} (${storeName}).`, + }); + + return; } - )) as string; + + this.logger.error( + { err: error }, + `Failed to resolve bridge secret key (store=${storeName}, cacheKey=${cacheKey}, environmentId=${environmentId})` + ); + res.status(500).json({ + error: 'Failed to resolve environment secret key', + details: `Unexpected error while loading secret via ${storeName} for cache key ${cacheKey}.`, + }); + + return; + } const novuRequestHandler = new NovuRequestHandler({ frameworkName, From 0cd8c3dc9f7864f8adf7674cd41f16326012edbe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 19:08:10 +0000 Subject: [PATCH 04/12] fix(api): drop invalid NODE_OPTIONS flag from mocha scripts Node disallows --no-experimental-strip-types in NODE_OPTIONS, which caused pnpm test / npx mocha to exit before running specs. Remove it from api test scripts and .mocharc.json; ts-node still transpiles via TS_NODE_TRANSPILE_ONLY. Co-authored-by: Dima Grossman --- apps/api/.mocharc.json | 1 - apps/api/package.json | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/api/.mocharc.json b/apps/api/.mocharc.json index 9d915c36a16..232a45a840e 100644 --- a/apps/api/.mocharc.json +++ b/apps/api/.mocharc.json @@ -1,7 +1,6 @@ { "timeout": 35000, "require": "@swc-node/register", - "node-option": ["no-experimental-strip-types"], "file": ["e2e/setup.ts"], "exit": true, "files": ["e2e/**/*.e2e.ts", "src/**/*.e2e.ts", "src/**/**/*.spec.ts"] diff --git a/apps/api/package.json b/apps/api/package.json index 48c7c013935..fee5c8bc4b7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -30,9 +30,9 @@ "pretest": "pnpm build:metadata", "generate:swagger": "ts-node exportOpenAPIJSON.ts", "generate:sdk": " (cd ../../libs/internal-sdk && speakeasy run --skip-compile --minimal --skip-versioning) && (cd ../../libs/internal-sdk && pnpm build) ", - "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true NODE_OPTIONS=--no-experimental-strip-types mocha --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", - "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test NODE_OPTIONS=--no-experimental-strip-types mocha --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", - "test:e2e:novu-v2": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS='--max_old_space_size=8192 --no-experimental-strip-types' node scripts/run-novu-v2-e2e-shard.cjs", + "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true mocha --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", + "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test mocha --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", + "test:e2e:novu-v2": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS=--max_old_space_size=8192 node scripts/run-novu-v2-e2e-shard.cjs", "migration": "cross-env NODE_ENV=local MIGRATION=true ts-node --transpileOnly", "seed:clickhouse": "cross-env NODE_ENV=local ts-node --transpileOnly scripts/seed-clickhouse.ts", "seed:triggers": "cross-env NODE_ENV=local ts-node --transpileOnly scripts/seed-triggers.ts", From 64650952531878a602aef1bac74ba1d72bb3429e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 19:19:53 +0000 Subject: [PATCH 05/12] fix(application-generic): invalidate workflow preferences LRU on upsert GetPreferences can use InMemoryLRUCacheStore.WORKFLOW_PREFERENCES for WORKFLOW_RESOURCE + USER_WORKFLOW rows. UpsertPreferences wrote to Mongo without clearing that cache, so subsequent reads could return stale data and fail tests/CI when preference-fetch optimization is enabled. Co-authored-by: Dima Grossman --- .../upsert-preferences.usecase.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts index 85d21b3a55a..4cca72a3e1a 100644 --- a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts +++ b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts @@ -15,6 +15,7 @@ import { import { FilterQuery } from 'mongoose'; import { Instrument } from '../../instrumentation'; import { FeatureFlagsService } from '../../services/feature-flags/feature-flags.service'; +import { InMemoryLRUCacheService, InMemoryLRUCacheStore } from '../../services/in-memory-lru-cache'; import { deepMerge } from '../../utils'; import { UpsertSubscriberGlobalPreferencesCommand } from './upsert-subscriber-global-preferences.command'; import { UpsertSubscriberWorkflowPreferencesCommand } from './upsert-subscriber-workflow-preferences.command'; @@ -45,7 +46,8 @@ type UpsertPreferencesCommand = Omit< export class UpsertPreferences { constructor( private preferencesRepository: PreferencesRepository, - private featureFlagsService: FeatureFlagsService + private featureFlagsService: FeatureFlagsService, + private inMemoryLRUCacheService: InMemoryLRUCacheService ) {} @Instrument() @@ -156,11 +158,34 @@ export class UpsertPreferences { private async upsert(command: UpsertPreferencesCommand): Promise { const foundPreference = await this.getPreference(command); + let result: PreferencesEntity | undefined; if (foundPreference) { - return this.updatePreferences(foundPreference, command); + result = await this.updatePreferences(foundPreference, command); + } else { + result = await this.createPreferences(command); } - return this.createPreferences(command); + this.invalidateWorkflowPreferencesCacheIfNeeded(command); + + return result; + } + + private invalidateWorkflowPreferencesCacheIfNeeded(command: UpsertPreferencesCommand): void { + if (!command.templateId) { + + return; + } + + if ( + command.type !== PreferencesTypeEnum.WORKFLOW_RESOURCE && + command.type !== PreferencesTypeEnum.USER_WORKFLOW + ) { + + return; + } + + const cacheKey = `${command.environmentId}:${command.templateId}`; + this.inMemoryLRUCacheService.invalidate(InMemoryLRUCacheStore.WORKFLOW_PREFERENCES, cacheKey); } private async createPreferences(command: UpsertPreferencesCommand): Promise { From 479f36721db852915fd8cb37b23f55c9d62a5028 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 19:36:12 +0000 Subject: [PATCH 06/12] fix(api): satisfy biome on NovuBridgeClient (PinoLogger, express handler types) - Replace Nest Logger with injected PinoLogger per API lint rules - Import SharedModule in NovuBridgeModule so LoggerModule/PinoLogger resolves - Cast framework createHandler to Express req/res without explicit any Co-authored-by: Dima Grossman --- .../src/app/environments-v1/novu-bridge-client.ts | 15 ++++++++++----- .../src/app/environments-v1/novu-bridge.module.ts | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/api/src/app/environments-v1/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts index 4228190448e..ddf123673db 100644 --- a/apps/api/src/app/environments-v1/novu-bridge-client.ts +++ b/apps/api/src/app/environments-v1/novu-bridge-client.ts @@ -1,9 +1,10 @@ -import { Inject, Logger, NotFoundException } from '@nestjs/common'; +import { Inject, NotFoundException } from '@nestjs/common'; import { GetDecryptedSecretKey, GetDecryptedSecretKeyCommand, InMemoryLRUCacheService, InMemoryLRUCacheStore, + PinoLogger, } from '@novu/application-generic'; import { PostActionEnum, type Workflow } from '@novu/framework/internal'; import { Client, NovuHandler, NovuRequestHandler } from '@novu/framework/nest'; @@ -22,13 +23,12 @@ export const frameworkName = 'novu-nest'; * workflows to serve on the Novu Bridge. */ export class NovuBridgeClient { - private readonly logger = new Logger(NovuBridgeClient.name); - constructor( @Inject(NovuHandler) private novuHandler: NovuHandler, private constructFrameworkWorkflow: ConstructFrameworkWorkflow, private getDecryptedSecretKey: GetDecryptedSecretKey, - private inMemoryLRUCacheService: InMemoryLRUCacheService + private inMemoryLRUCacheService: InMemoryLRUCacheService, + private logger: PinoLogger ) {} public async handleRequest(req: Request, res: Response) { @@ -131,6 +131,11 @@ export class NovuBridgeClient { handler: this.novuHandler.handler, }); - await novuRequestHandler.createHandler()(req as any, res as any); + const bridgeHandler = novuRequestHandler.createHandler() as ( + request: Request, + response: Response + ) => void | Promise; + + await bridgeHandler(req, res); } } diff --git a/apps/api/src/app/environments-v1/novu-bridge.module.ts b/apps/api/src/app/environments-v1/novu-bridge.module.ts index 02e654f8724..6587218e974 100644 --- a/apps/api/src/app/environments-v1/novu-bridge.module.ts +++ b/apps/api/src/app/environments-v1/novu-bridge.module.ts @@ -25,6 +25,7 @@ import { } from '@novu/dal'; import { NovuClient, NovuHandler } from '@novu/framework/nest'; import { GetOrganizationSettings } from '../organization/usecases/get-organization-settings/get-organization-settings.usecase'; +import { SharedModule } from '../shared/shared.module'; import { NovuBridgeController } from './novu-bridge.controller'; import { NovuBridgeClient } from './novu-bridge-client'; import { ConstructFrameworkWorkflow } from './usecases/construct-framework-workflow'; @@ -50,6 +51,7 @@ export const featureFlagsService = { }; @Module({ + imports: [SharedModule], controllers: [NovuBridgeController], providers: [ { From 1880971ea883257f2a1eeeeb797ec87187484697 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 19:43:52 +0000 Subject: [PATCH 07/12] fix(api): unblock CI lint and novu-v2 billing e2e - Remove unused SENSITIVE_KEYS from payload-sanitizer (Biome error in nx lint) - Replace require() with dynamic import in get-portal-link e2e for ESM compatibility Co-authored-by: Dima Grossman --- apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts | 7 ++++--- apps/api/src/utils/payload-sanitizer.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts b/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts index 403ec34caa4..4b1034cb31b 100644 --- a/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts +++ b/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts @@ -3,9 +3,10 @@ import sinon from 'sinon'; const dashboardOrigin = process.env.DASHBOARD_URL; -describe('Get portal link #novu-v2', async () => { +describe('Get portal link #novu-v2', () => { it('Get portal link', async () => { - if (!require('@novu/ee-billing').GetPortalLink) { + const { GetPortalLink } = await import('@novu/ee-billing'); + if (!GetPortalLink) { throw new Error("GetPortalLink doesn't exist"); } const stubObject = { @@ -27,7 +28,7 @@ describe('Get portal link #novu-v2', async () => { const stub = sinon.stub(stubObject.billingPortal.sessions, 'create').resolves({ url: 'url' }); - const usecase = new (require('@novu/ee-billing').GetPortalLink)(stubObject, getCustomerUsecase); + const usecase = new GetPortalLink(stubObject, getCustomerUsecase); const result = await usecase.execute({ environmentId: 'environment_dd', diff --git a/apps/api/src/utils/payload-sanitizer.ts b/apps/api/src/utils/payload-sanitizer.ts index 101326da6e8..f9e03473cd8 100644 --- a/apps/api/src/utils/payload-sanitizer.ts +++ b/apps/api/src/utils/payload-sanitizer.ts @@ -1,8 +1,9 @@ -const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apikey', 'email', 'phone', 'bearer']; const MAX_PAYLOAD_SIZE = 51200; // 50KB export function sanitizePayload(payload: Record): string { - if (!payload) return ''; + if (!payload) { + return ''; + } try { let str = JSON.stringify(payload); From d4bcd3dfd3402c7a117d1eac6214a47c0ecc7fac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 20:03:23 +0000 Subject: [PATCH 08/12] fix(shared): skip New Relic require in CI test runs GitHub Actions sets CI=true and NODE_ENV=test; loading newrelic without NEW_RELIC_APP_NAME throws at import time and fails @novu/api-service tests. Use a noop agent in that environment for metrics and cron modules, matching clickhouse-batch.service.ts. Co-authored-by: Dima Grossman --- .../src/services/cron/cron.service.ts | 3 ++- .../services/load-newrelic-ci-test-safe.ts | 25 +++++++++++++++++++ .../src/services/metrics/metrics.service.ts | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 libs/application-generic/src/services/load-newrelic-ci-test-safe.ts diff --git a/libs/application-generic/src/services/cron/cron.service.ts b/libs/application-generic/src/services/cron/cron.service.ts index 5f6233c16c1..59809f4976b 100644 --- a/libs/application-generic/src/services/cron/cron.service.ts +++ b/libs/application-generic/src/services/cron/cron.service.ts @@ -6,10 +6,11 @@ import { TimezoneEnum, } from '@novu/shared'; import { captureException } from '@sentry/node'; +import { loadNewRelicOrNoopInCiTest } from '../load-newrelic-ci-test-safe'; import { MetricsService } from '../metrics'; import { CronJobData, CronJobProcessor, CronMetrics, CronMetricsEventEnum, CronOptions } from './cron.types'; -const nr = require('newrelic'); +const nr = loadNewRelicOrNoopInCiTest(); const LOG_CONTEXT = 'CronService'; const DEFAULT_CRON_OPTIONS: CronOptions = { diff --git a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts new file mode 100644 index 00000000000..55d00ccf6f0 --- /dev/null +++ b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts @@ -0,0 +1,25 @@ +/** + * New Relic's agent throws at require() time when NEW_RELIC_APP_NAME is unset. + * CI runs `nx test` without that env; use no-op implementations so imports do not crash. + * Mirrors the pattern in analytic-logs/clickhouse-batch.service.ts. + */ +const noopTransaction = { end: () => {} }; + +export const noopNewRelicForCiTest = { + startBackgroundTransaction: (_transactionName: string, _groupName: string, callback: () => void) => callback(), + getTransaction: () => noopTransaction, + noticeError: (_error: unknown) => {}, + recordMetric: (_name: string, _value: number) => {}, +}; + +export type NewRelicAgentLike = typeof noopNewRelicForCiTest; + +export function loadNewRelicOrNoopInCiTest(): NewRelicAgentLike { + if (process.env.CI && process.env.NODE_ENV === 'test') { + return noopNewRelicForCiTest; + } + + // New Relic is CommonJS-only; dynamic import would be async and break call sites. + // biome-ignore lint/style/noCommonJs: newrelic package has no ESM entry + return require('newrelic') as NewRelicAgentLike; +} diff --git a/libs/application-generic/src/services/metrics/metrics.service.ts b/libs/application-generic/src/services/metrics/metrics.service.ts index 8e36dbf5133..57eca3c8d5c 100644 --- a/libs/application-generic/src/services/metrics/metrics.service.ts +++ b/libs/application-generic/src/services/metrics/metrics.service.ts @@ -1,8 +1,9 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import * as otelApi from '@opentelemetry/api'; +import { loadNewRelicOrNoopInCiTest } from '../load-newrelic-ci-test-safe'; import { IMetricsService } from './metrics.interface'; -const nr = require('newrelic'); +const nr = loadNewRelicOrNoopInCiTest(); const LOG_CONTEXT = 'MetricsService'; From b105d73490b269b6ceeb65b75d91441ac81c8fdc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 20:13:31 +0000 Subject: [PATCH 09/12] fix(shared): avoid newrelic import in redis provider during CI tests in-memory-provider/redis-provider used a static newrelic import, which still loaded the agent without NEW_RELIC_APP_NAME and failed CI. Use loadNewRelicOrNoopInCiTest and extend the noop type for optional instrumentDatastore. Co-authored-by: Dima Grossman --- .../services/in-memory-provider/providers/redis-provider.ts | 4 +++- .../src/services/load-newrelic-ci-test-safe.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts b/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts index 61b21ded0ac..8efc3bd7af9 100644 --- a/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts +++ b/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts @@ -1,9 +1,11 @@ import Redis, { RedisOptions, ScanStream } from 'ioredis'; -import newrelic from 'newrelic'; import { ConnectionOptions } from 'tls'; +import { loadNewRelicOrNoopInCiTest } from '../../load-newrelic-ci-test-safe'; import { convertStringValues } from './variable-mappers'; +const newrelic = loadNewRelicOrNoopInCiTest(); + export { Redis, RedisOptions, ScanStream }; export const CLIENT_READY = 'ready'; diff --git a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts index 55d00ccf6f0..d490198976a 100644 --- a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts +++ b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts @@ -12,7 +12,9 @@ export const noopNewRelicForCiTest = { recordMetric: (_name: string, _value: number) => {}, }; -export type NewRelicAgentLike = typeof noopNewRelicForCiTest; +export type NewRelicAgentLike = typeof noopNewRelicForCiTest & { + instrumentDatastore?: (name: string, callback: () => unknown) => void; +}; export function loadNewRelicOrNoopInCiTest(): NewRelicAgentLike { if (process.env.CI && process.env.NODE_ENV === 'test') { From 14efa9385d9c43fe511970afc662d5f7a53ed432 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 20:23:41 +0000 Subject: [PATCH 10/12] fix(shared): use CI-safe New Relic load in instrumentation decorator require("newrelic") throws during module init when NEW_RELIC_APP_NAME is unset, so the decorator try/catch never caught it. Use loadNewRelicOrNoopInCiTest and add startSegment to the noop agent. Co-authored-by: Dima Grossman --- .../src/instrumentation/instrumentation.decorator.ts | 7 ++++--- .../src/services/load-newrelic-ci-test-safe.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/application-generic/src/instrumentation/instrumentation.decorator.ts b/libs/application-generic/src/instrumentation/instrumentation.decorator.ts index 2c66632b1af..253770ad71f 100644 --- a/libs/application-generic/src/instrumentation/instrumentation.decorator.ts +++ b/libs/application-generic/src/instrumentation/instrumentation.decorator.ts @@ -1,5 +1,7 @@ import 'reflect-metadata'; +import { loadNewRelicOrNoopInCiTest } from '../services/load-newrelic-ci-test-safe'; + function copyMetadata(source: any, target: any): void { const result = Reflect.getMetadataKeys(source); @@ -41,10 +43,9 @@ function instrumentationWrapper({ const transactionIdentifierBase = `${instrumentationType}/${target?.constructor?.name}/${methodName}`; - let nr: any = null; + let nr: ReturnType | null = null; try { - // Dynamically load newrelic - nr = require('newrelic'); + nr = loadNewRelicOrNoopInCiTest(); } catch { return descriptor; } diff --git a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts index d490198976a..bcd8acc090f 100644 --- a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts +++ b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts @@ -10,6 +10,7 @@ export const noopNewRelicForCiTest = { getTransaction: () => noopTransaction, noticeError: (_error: unknown) => {}, recordMetric: (_name: string, _value: number) => {}, + startSegment: (_name: string, _record: boolean, handler: () => unknown) => handler(), }; export type NewRelicAgentLike = typeof noopNewRelicForCiTest & { From fd178ff1c73419a78106f5fa4e4346e45878c727 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 11 Apr 2026 20:46:32 +0000 Subject: [PATCH 11/12] fix(api): disable Node 22 native TS stripping for mocha on Node 22+ Node 22 can execute .ts via experimental type-stripping before ts-node, which fails on decorator syntax. Spawn mocha with --no-experimental-strip-types only when Node major >= 22 so local Node 20 stays valid. Apply the same flag to the novu-v2 e2e shard runner. Co-authored-by: Dima Grossman --- apps/api/package.json | 4 ++-- apps/api/scripts/run-mocha-unit.cjs | 15 +++++++++++++++ apps/api/scripts/run-novu-v2-e2e-shard.cjs | 8 +++++++- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 apps/api/scripts/run-mocha-unit.cjs diff --git a/apps/api/package.json b/apps/api/package.json index fee5c8bc4b7..15f52cdbef8 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -30,8 +30,8 @@ "pretest": "pnpm build:metadata", "generate:swagger": "ts-node exportOpenAPIJSON.ts", "generate:sdk": " (cd ../../libs/internal-sdk && speakeasy run --skip-compile --minimal --skip-versioning) && (cd ../../libs/internal-sdk && pnpm build) ", - "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true mocha --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", - "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test mocha --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", + "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true node scripts/run-mocha-unit.cjs --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", + "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test node scripts/run-mocha-unit.cjs --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", "test:e2e:novu-v2": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS=--max_old_space_size=8192 node scripts/run-novu-v2-e2e-shard.cjs", "migration": "cross-env NODE_ENV=local MIGRATION=true ts-node --transpileOnly", "seed:clickhouse": "cross-env NODE_ENV=local ts-node --transpileOnly scripts/seed-clickhouse.ts", diff --git a/apps/api/scripts/run-mocha-unit.cjs b/apps/api/scripts/run-mocha-unit.cjs new file mode 100644 index 00000000000..66587e5da54 --- /dev/null +++ b/apps/api/scripts/run-mocha-unit.cjs @@ -0,0 +1,15 @@ +/** + * Node 22+ may run .ts through native type-stripping before ts-node, which breaks decorators. + * Disable stripping only when supported (Node 22+). Node 20 has no such flag. + */ +const { spawnSync } = require('node:child_process'); +const { join } = require('node:path'); + +const major = Number.parseInt(process.versions.node.split('.')[0], 10); +const mochaBin = join(__dirname, '..', 'node_modules', 'mocha', 'bin', '_mocha'); +const nodeArgs = major >= 22 ? ['--no-experimental-strip-types'] : []; +const args = [...nodeArgs, mochaBin, ...process.argv.slice(2)]; + +const result = spawnSync(process.execPath, args, { stdio: 'inherit', shell: false }); + +process.exit(result.status === null ? 1 : result.status); diff --git a/apps/api/scripts/run-novu-v2-e2e-shard.cjs b/apps/api/scripts/run-novu-v2-e2e-shard.cjs index 7396997e65e..1f4a756ab25 100644 --- a/apps/api/scripts/run-novu-v2-e2e-shard.cjs +++ b/apps/api/scripts/run-novu-v2-e2e-shard.cjs @@ -190,8 +190,14 @@ function printShardSummary(shardIndex, totalShards, shard) { console.log(`Running Novu V2 E2E shard ${shardIndex}/${totalShards} with ${shard.files.length} files (weight ${shard.weight}).`); } +function getNodeMajor() { + return Number.parseInt(process.versions.node.split('.')[0], 10); +} + function runMocha(filePaths) { - return spawnSync(process.execPath, [require.resolve('mocha/bin/mocha'), ...MOCHA_ARGS, ...filePaths], { + const nodeFlags = getNodeMajor() >= 22 ? ['--no-experimental-strip-types'] : []; + + return spawnSync(process.execPath, [...nodeFlags, require.resolve('mocha/bin/mocha'), ...MOCHA_ARGS, ...filePaths], { cwd: ROOT, env: process.env, stdio: 'inherit', From 3c98023744cd05fe40ccce7f1c1660460d066079 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 12 Apr 2026 15:13:42 +0000 Subject: [PATCH 12/12] revert: drop CI/test and unrelated fixes; keep bridge auth only Restore api test scripts, e2e, payload-sanitizer, preferences cache, and application-generic New Relic shims to match next. Removes run-mocha-unit.cjs and load-newrelic-ci-test-safe.ts per review scope. Co-authored-by: Dima Grossman --- apps/api/.mocharc.json | 1 + apps/api/package.json | 8 ++--- apps/api/scripts/run-mocha-unit.cjs | 15 --------- apps/api/scripts/run-novu-v2-e2e-shard.cjs | 8 +---- .../app/billing/e2e/get-portal-link.e2e-ee.ts | 7 ++--- apps/api/src/utils/payload-sanitizer.ts | 5 ++- .../instrumentation.decorator.ts | 7 ++--- .../src/services/cron/cron.service.ts | 3 +- .../providers/redis-provider.ts | 4 +-- .../services/load-newrelic-ci-test-safe.ts | 28 ----------------- .../src/services/metrics/metrics.service.ts | 3 +- .../upsert-preferences.usecase.ts | 31 ++----------------- 12 files changed, 20 insertions(+), 100 deletions(-) delete mode 100644 apps/api/scripts/run-mocha-unit.cjs delete mode 100644 libs/application-generic/src/services/load-newrelic-ci-test-safe.ts diff --git a/apps/api/.mocharc.json b/apps/api/.mocharc.json index 232a45a840e..9d915c36a16 100644 --- a/apps/api/.mocharc.json +++ b/apps/api/.mocharc.json @@ -1,6 +1,7 @@ { "timeout": 35000, "require": "@swc-node/register", + "node-option": ["no-experimental-strip-types"], "file": ["e2e/setup.ts"], "exit": true, "files": ["e2e/**/*.e2e.ts", "src/**/*.e2e.ts", "src/**/**/*.spec.ts"] diff --git a/apps/api/package.json b/apps/api/package.json index 15f52cdbef8..beb702a6780 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -30,9 +30,9 @@ "pretest": "pnpm build:metadata", "generate:swagger": "ts-node exportOpenAPIJSON.ts", "generate:sdk": " (cd ../../libs/internal-sdk && speakeasy run --skip-compile --minimal --skip-versioning) && (cd ../../libs/internal-sdk && pnpm build) ", - "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true node scripts/run-mocha-unit.cjs --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", - "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test node scripts/run-mocha-unit.cjs --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", - "test:e2e:novu-v2": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS=--max_old_space_size=8192 node scripts/run-novu-v2-e2e-shard.cjs", + "test": "cross-env TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test NOVU_ENTERPRISE=true CLERK_ENABLED=true NODE_OPTIONS=--no-experimental-strip-types mocha --timeout 15000 --require ts-node/register --exit 'src/**/*.spec.ts'", + "test:e2e:novu-v0": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test NODE_OPTIONS=--no-experimental-strip-types mocha --timeout 15000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts", + "test:e2e:novu-v2": "cross-env TS_NODE_PROJECT=tsconfig.spec.json NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS='--max_old_space_size=8192 --no-experimental-strip-types' node scripts/run-novu-v2-e2e-shard.cjs", "migration": "cross-env NODE_ENV=local MIGRATION=true ts-node --transpileOnly", "seed:clickhouse": "cross-env NODE_ENV=local ts-node --transpileOnly scripts/seed-clickhouse.ts", "seed:triggers": "cross-env NODE_ENV=local ts-node --transpileOnly scripts/seed-triggers.ts", @@ -96,7 +96,7 @@ "handlebars": "4.7.9", "helmet": "^6.0.1", "i18next": "^23.7.6", - "ioredis": "5.3.2", + "ioredis": "5.10.1", "json-logic-js": "^2.0.5", "json-schema-faker": "^0.5.6", "json-schema-to-ts": "^3.0.0", diff --git a/apps/api/scripts/run-mocha-unit.cjs b/apps/api/scripts/run-mocha-unit.cjs deleted file mode 100644 index 66587e5da54..00000000000 --- a/apps/api/scripts/run-mocha-unit.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Node 22+ may run .ts through native type-stripping before ts-node, which breaks decorators. - * Disable stripping only when supported (Node 22+). Node 20 has no such flag. - */ -const { spawnSync } = require('node:child_process'); -const { join } = require('node:path'); - -const major = Number.parseInt(process.versions.node.split('.')[0], 10); -const mochaBin = join(__dirname, '..', 'node_modules', 'mocha', 'bin', '_mocha'); -const nodeArgs = major >= 22 ? ['--no-experimental-strip-types'] : []; -const args = [...nodeArgs, mochaBin, ...process.argv.slice(2)]; - -const result = spawnSync(process.execPath, args, { stdio: 'inherit', shell: false }); - -process.exit(result.status === null ? 1 : result.status); diff --git a/apps/api/scripts/run-novu-v2-e2e-shard.cjs b/apps/api/scripts/run-novu-v2-e2e-shard.cjs index 1f4a756ab25..7396997e65e 100644 --- a/apps/api/scripts/run-novu-v2-e2e-shard.cjs +++ b/apps/api/scripts/run-novu-v2-e2e-shard.cjs @@ -190,14 +190,8 @@ function printShardSummary(shardIndex, totalShards, shard) { console.log(`Running Novu V2 E2E shard ${shardIndex}/${totalShards} with ${shard.files.length} files (weight ${shard.weight}).`); } -function getNodeMajor() { - return Number.parseInt(process.versions.node.split('.')[0], 10); -} - function runMocha(filePaths) { - const nodeFlags = getNodeMajor() >= 22 ? ['--no-experimental-strip-types'] : []; - - return spawnSync(process.execPath, [...nodeFlags, require.resolve('mocha/bin/mocha'), ...MOCHA_ARGS, ...filePaths], { + return spawnSync(process.execPath, [require.resolve('mocha/bin/mocha'), ...MOCHA_ARGS, ...filePaths], { cwd: ROOT, env: process.env, stdio: 'inherit', diff --git a/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts b/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts index 4b1034cb31b..403ec34caa4 100644 --- a/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts +++ b/apps/api/src/app/billing/e2e/get-portal-link.e2e-ee.ts @@ -3,10 +3,9 @@ import sinon from 'sinon'; const dashboardOrigin = process.env.DASHBOARD_URL; -describe('Get portal link #novu-v2', () => { +describe('Get portal link #novu-v2', async () => { it('Get portal link', async () => { - const { GetPortalLink } = await import('@novu/ee-billing'); - if (!GetPortalLink) { + if (!require('@novu/ee-billing').GetPortalLink) { throw new Error("GetPortalLink doesn't exist"); } const stubObject = { @@ -28,7 +27,7 @@ describe('Get portal link #novu-v2', () => { const stub = sinon.stub(stubObject.billingPortal.sessions, 'create').resolves({ url: 'url' }); - const usecase = new GetPortalLink(stubObject, getCustomerUsecase); + const usecase = new (require('@novu/ee-billing').GetPortalLink)(stubObject, getCustomerUsecase); const result = await usecase.execute({ environmentId: 'environment_dd', diff --git a/apps/api/src/utils/payload-sanitizer.ts b/apps/api/src/utils/payload-sanitizer.ts index f9e03473cd8..101326da6e8 100644 --- a/apps/api/src/utils/payload-sanitizer.ts +++ b/apps/api/src/utils/payload-sanitizer.ts @@ -1,9 +1,8 @@ +const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apikey', 'email', 'phone', 'bearer']; const MAX_PAYLOAD_SIZE = 51200; // 50KB export function sanitizePayload(payload: Record): string { - if (!payload) { - return ''; - } + if (!payload) return ''; try { let str = JSON.stringify(payload); diff --git a/libs/application-generic/src/instrumentation/instrumentation.decorator.ts b/libs/application-generic/src/instrumentation/instrumentation.decorator.ts index 253770ad71f..2c66632b1af 100644 --- a/libs/application-generic/src/instrumentation/instrumentation.decorator.ts +++ b/libs/application-generic/src/instrumentation/instrumentation.decorator.ts @@ -1,7 +1,5 @@ import 'reflect-metadata'; -import { loadNewRelicOrNoopInCiTest } from '../services/load-newrelic-ci-test-safe'; - function copyMetadata(source: any, target: any): void { const result = Reflect.getMetadataKeys(source); @@ -43,9 +41,10 @@ function instrumentationWrapper({ const transactionIdentifierBase = `${instrumentationType}/${target?.constructor?.name}/${methodName}`; - let nr: ReturnType | null = null; + let nr: any = null; try { - nr = loadNewRelicOrNoopInCiTest(); + // Dynamically load newrelic + nr = require('newrelic'); } catch { return descriptor; } diff --git a/libs/application-generic/src/services/cron/cron.service.ts b/libs/application-generic/src/services/cron/cron.service.ts index 59809f4976b..5f6233c16c1 100644 --- a/libs/application-generic/src/services/cron/cron.service.ts +++ b/libs/application-generic/src/services/cron/cron.service.ts @@ -6,11 +6,10 @@ import { TimezoneEnum, } from '@novu/shared'; import { captureException } from '@sentry/node'; -import { loadNewRelicOrNoopInCiTest } from '../load-newrelic-ci-test-safe'; import { MetricsService } from '../metrics'; import { CronJobData, CronJobProcessor, CronMetrics, CronMetricsEventEnum, CronOptions } from './cron.types'; -const nr = loadNewRelicOrNoopInCiTest(); +const nr = require('newrelic'); const LOG_CONTEXT = 'CronService'; const DEFAULT_CRON_OPTIONS: CronOptions = { diff --git a/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts b/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts index 8efc3bd7af9..61b21ded0ac 100644 --- a/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts +++ b/libs/application-generic/src/services/in-memory-provider/providers/redis-provider.ts @@ -1,11 +1,9 @@ import Redis, { RedisOptions, ScanStream } from 'ioredis'; +import newrelic from 'newrelic'; import { ConnectionOptions } from 'tls'; -import { loadNewRelicOrNoopInCiTest } from '../../load-newrelic-ci-test-safe'; import { convertStringValues } from './variable-mappers'; -const newrelic = loadNewRelicOrNoopInCiTest(); - export { Redis, RedisOptions, ScanStream }; export const CLIENT_READY = 'ready'; diff --git a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts b/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts deleted file mode 100644 index bcd8acc090f..00000000000 --- a/libs/application-generic/src/services/load-newrelic-ci-test-safe.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * New Relic's agent throws at require() time when NEW_RELIC_APP_NAME is unset. - * CI runs `nx test` without that env; use no-op implementations so imports do not crash. - * Mirrors the pattern in analytic-logs/clickhouse-batch.service.ts. - */ -const noopTransaction = { end: () => {} }; - -export const noopNewRelicForCiTest = { - startBackgroundTransaction: (_transactionName: string, _groupName: string, callback: () => void) => callback(), - getTransaction: () => noopTransaction, - noticeError: (_error: unknown) => {}, - recordMetric: (_name: string, _value: number) => {}, - startSegment: (_name: string, _record: boolean, handler: () => unknown) => handler(), -}; - -export type NewRelicAgentLike = typeof noopNewRelicForCiTest & { - instrumentDatastore?: (name: string, callback: () => unknown) => void; -}; - -export function loadNewRelicOrNoopInCiTest(): NewRelicAgentLike { - if (process.env.CI && process.env.NODE_ENV === 'test') { - return noopNewRelicForCiTest; - } - - // New Relic is CommonJS-only; dynamic import would be async and break call sites. - // biome-ignore lint/style/noCommonJs: newrelic package has no ESM entry - return require('newrelic') as NewRelicAgentLike; -} diff --git a/libs/application-generic/src/services/metrics/metrics.service.ts b/libs/application-generic/src/services/metrics/metrics.service.ts index 57eca3c8d5c..8e36dbf5133 100644 --- a/libs/application-generic/src/services/metrics/metrics.service.ts +++ b/libs/application-generic/src/services/metrics/metrics.service.ts @@ -1,9 +1,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import * as otelApi from '@opentelemetry/api'; -import { loadNewRelicOrNoopInCiTest } from '../load-newrelic-ci-test-safe'; import { IMetricsService } from './metrics.interface'; -const nr = loadNewRelicOrNoopInCiTest(); +const nr = require('newrelic'); const LOG_CONTEXT = 'MetricsService'; diff --git a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts index 4cca72a3e1a..85d21b3a55a 100644 --- a/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts +++ b/libs/application-generic/src/usecases/upsert-preferences/upsert-preferences.usecase.ts @@ -15,7 +15,6 @@ import { import { FilterQuery } from 'mongoose'; import { Instrument } from '../../instrumentation'; import { FeatureFlagsService } from '../../services/feature-flags/feature-flags.service'; -import { InMemoryLRUCacheService, InMemoryLRUCacheStore } from '../../services/in-memory-lru-cache'; import { deepMerge } from '../../utils'; import { UpsertSubscriberGlobalPreferencesCommand } from './upsert-subscriber-global-preferences.command'; import { UpsertSubscriberWorkflowPreferencesCommand } from './upsert-subscriber-workflow-preferences.command'; @@ -46,8 +45,7 @@ type UpsertPreferencesCommand = Omit< export class UpsertPreferences { constructor( private preferencesRepository: PreferencesRepository, - private featureFlagsService: FeatureFlagsService, - private inMemoryLRUCacheService: InMemoryLRUCacheService + private featureFlagsService: FeatureFlagsService ) {} @Instrument() @@ -158,34 +156,11 @@ export class UpsertPreferences { private async upsert(command: UpsertPreferencesCommand): Promise { const foundPreference = await this.getPreference(command); - let result: PreferencesEntity | undefined; if (foundPreference) { - result = await this.updatePreferences(foundPreference, command); - } else { - result = await this.createPreferences(command); + return this.updatePreferences(foundPreference, command); } - this.invalidateWorkflowPreferencesCacheIfNeeded(command); - - return result; - } - - private invalidateWorkflowPreferencesCacheIfNeeded(command: UpsertPreferencesCommand): void { - if (!command.templateId) { - - return; - } - - if ( - command.type !== PreferencesTypeEnum.WORKFLOW_RESOURCE && - command.type !== PreferencesTypeEnum.USER_WORKFLOW - ) { - - return; - } - - const cacheKey = `${command.environmentId}:${command.templateId}`; - this.inMemoryLRUCacheService.invalidate(InMemoryLRUCacheStore.WORKFLOW_PREFERENCES, cacheKey); + return this.createPreferences(command); } private async createPreferences(command: UpsertPreferencesCommand): Promise {