Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,10 @@
"checkpointer",
"langchain",
"langgraph",
"Vitest"
"Vitest",
"signoz",
"sanitised",
"serialises"
],
"flagWords": [],
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.0.0",
"jsonwebtoken": "9.0.3",
"liquidjs": "^10.20.0",
"liquidjs": "^10.25.0",
"lodash": "^4.17.15",
"lru-cache": "^11.2.4",
"nanoid": "^3.1.20",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export const envValidators = {
WORKER_DEFAULT_CONCURRENCY: num({ default: undefined }),
WORKER_DEFAULT_LOCK_DURATION: num({ default: undefined }),
ENABLE_OTEL: bool({ default: false }),
ENABLE_OTEL_LOGS: bool({ default: false }),
OTEL_PROMETHEUS_PORT: num({ default: 9464 }),
NOTIFICATION_RETENTION_DAYS: num({ default: DEFAULT_NOTIFICATION_RETENTION_DAYS }),
API_ROOT_URL: url(),
NOVU_INVITE_TEAM_MEMBER_NUDGE_TRIGGER_IDENTIFIER: str({ default: undefined }),
Expand Down
16 changes: 14 additions & 2 deletions apps/api/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import './config/env.config';

// Import from the tracing subpath, NOT the main barrel. The barrel loads
// @novu/application-generic which transitively pulls in pino/mongoose/ioredis.
// TypeScript hoists all imports — if pino loads before startOtel() registers
// instrumentations, PinoInstrumentation cannot patch the already-bound references.
// Importing only otel-init keeps those modules out of require.cache until after
// the SDK's require()-hooks are in place.
import { startOtel } from '@novu/application-generic/build/main/tracing/otel-init';
import { name, version } from '../package.json';

startOtel(name, version);

// biome-ignore lint: must execute after startOtel() so New Relic layers on top
require('newrelic');

import { init } from '@sentry/nestjs';
import { version } from '../package.json';
// biome-ignore lint: lazy require so @sentry/nestjs loads after OTEL instrumentations are installed
const { init } = require('@sentry/nestjs');

if (process.env.SENTRY_DSN) {
init({
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"json-schema": "^0.4.0",
"json5": "^2.2.3",
"launchdarkly-react-client-sdk": "^3.9.0",
"liquidjs": "^10.20.0",
"liquidjs": "^10.25.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
Expand Down
12 changes: 3 additions & 9 deletions apps/dashboard/src/components/workflow-editor/add-step-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { FeatureFlagsKeysEnum } from '@novu/shared';
import { PopoverPortal } from '@radix-ui/react-popover';
import React, { ReactNode, useState } from 'react';
import { RiAddLine } from 'react-icons/ri';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { STEP_TYPE_TO_COLOR } from '@/utils/color';
import { StepTypeEnum } from '@/utils/enums';
import { cn } from '@/utils/ui';
Expand Down Expand Up @@ -82,8 +80,6 @@ export const AddStepMenu = ({
onMenuItemClick: (stepType: StepTypeEnum) => void;
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const isThrottleStepEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_THROTTLE_STEP_ENABLED);

const handleMenuItemClick = (stepType: StepTypeEnum) => {
onMenuItemClick(stepType);
setIsPopoverOpen(false);
Expand Down Expand Up @@ -148,11 +144,9 @@ export const AddStepMenu = ({
<MenuItem stepType={StepTypeEnum.DIGEST} onClick={() => handleMenuItemClick(StepTypeEnum.DIGEST)}>
Digest
</MenuItem>
{isThrottleStepEnabled && (
<MenuItem stepType={StepTypeEnum.THROTTLE} onClick={() => handleMenuItemClick(StepTypeEnum.THROTTLE)}>
Throttle
</MenuItem>
)}
<MenuItem stepType={StepTypeEnum.THROTTLE} onClick={() => handleMenuItemClick(StepTypeEnum.THROTTLE)}>
Throttle
</MenuItem>
</MenuItemsGroup>
</MenuGroup>
</div>
Expand Down
12 changes: 7 additions & 5 deletions apps/dashboard/src/utils/better-auth/components/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/primitives/button';
import { Input } from '@/components/primitives/input';
Expand All @@ -8,6 +8,8 @@ import { authClient } from '../client';

export function SignIn() {
const navigate = useNavigate();
const emailId = useId();
const passwordId = useId();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -82,12 +84,12 @@ export function SignIn() {
<h2 className="mb-6 text-center text-xl font-semibold">Sign In</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground-700">
<label htmlFor={emailId} className="mb-1 block text-sm font-medium text-foreground-700">
Email
</label>
<Input
type="email"
id="email"
id={emailId}
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
placeholder="user@example.com"
Expand All @@ -97,7 +99,7 @@ export function SignIn() {
</div>
<div>
<div className="mb-1 flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium text-foreground-700">
<label htmlFor={passwordId} className="block text-sm font-medium text-foreground-700">
Password
</label>
<span
Expand All @@ -114,7 +116,7 @@ export function SignIn() {
</div>
<Input
type="password"
id="password"
id={passwordId}
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
placeholder="Password"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useId, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Button } from '@/components/primitives/button';
import { Input } from '@/components/primitives/input';
Expand All @@ -8,6 +8,7 @@ import { authClient } from '../client';
export function SSOSignIn() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const ssoEmailId = useId();
const [email, setEmail] = useState('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -62,12 +63,12 @@ export function SSOSignIn() {
<h2 className="mb-6 text-center text-xl font-semibold">Sign In with SSO</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="sso-email" className="mb-1 block text-sm font-medium text-foreground-700">
<label htmlFor={ssoEmailId} className="mb-1 block text-sm font-medium text-foreground-700">
Work Email
</label>
<Input
type="email"
id="sso-email"
id={ssoEmailId}
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
placeholder="you@company.com"
Expand Down
8 changes: 6 additions & 2 deletions apps/dashboard/src/utils/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function recursiveGetUniqueFields(query: RuleGroupType): string[] {
if ('rules' in rule) {
// recursively get fields from nested rule groups
const nestedFields = recursiveGetUniqueFields(rule);
nestedFields.forEach((field) => fields.add(field));
for (const field of nestedFields) {
fields.add(field);
}
} else {
// add field from individual rule
const field = rule.field.split('.').shift();
Expand Down Expand Up @@ -109,7 +111,9 @@ function recursiveGetUniqueOperators(query: RuleGroupType): string[] {
if ('rules' in rule) {
// recursively get operators from nested rule groups
const nestedOperators = recursiveGetUniqueOperators(rule);
nestedOperators.forEach((operator) => operators.add(operator));
for (const operator of nestedOperators) {
operators.add(operator);
}
} else {
// add operator from individual rule
operators.add(rule.operator);
Expand Down
2 changes: 2 additions & 0 deletions apps/webhook/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export async function bootstrap(): Promise<INestApplication> {
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
});

app.enableShutdownHooks();

await app.listen(process.env.PORT);

return app;
Expand Down
16 changes: 14 additions & 2 deletions apps/webhook/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import './config/env.config';
import { init } from '@sentry/nestjs';
import { version } from '../package.json';

// Import from the tracing subpath, NOT the main barrel. The barrel loads
// @novu/application-generic which transitively pulls in pino/mongoose/ioredis.
// TypeScript hoists all imports — if pino loads before startOtel() registers
// instrumentations, PinoInstrumentation cannot patch the already-bound references.
// Importing only otel-init keeps those modules out of require.cache until after
// the SDK's require()-hooks are in place.
import { startOtel } from '@novu/application-generic/build/main/tracing/otel-init';
import { name, version } from '../package.json';

startOtel(name, version);

// biome-ignore lint: lazy require so @sentry/nestjs loads after OTEL instrumentations are installed
const { init } = require('@sentry/nestjs');

if (process.env.SENTRY_DSN) {
init({
Expand Down
3 changes: 3 additions & 0 deletions apps/worker/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import {

import { APP_FILTER } from '@nestjs/core';
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
import { TracingModule } from '@novu/application-generic';
import { HealthModule } from './app/health/health.module';
import { SharedModule } from './app/shared/shared.module';
import { TelemetryModule } from './app/telemetry/telemetry.module';
import { WorkflowModule } from './app/workflow/workflow.module';
import packageJson from '../package.json';

const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [
SharedModule,
HealthModule,
WorkflowModule,
TelemetryModule,
TracingModule.register(packageJson.name, packageJson.version),
];

const providers: Provider[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export class ActiveJobsMetricService {
public readonly activeJobsMetricWorkerService: ActiveJobsMetricWorkerService,
private metricsService: MetricsService
) {
if (process.env.NOVU_MANAGED_SERVICE === 'true' && process.env.NEW_RELIC_LICENSE_KEY) {
const hasMetricsBackend =
(process.env.NOVU_MANAGED_SERVICE === 'true' && !!process.env.NEW_RELIC_LICENSE_KEY) ||
process.env.ENABLE_OTEL === 'true';

if (hasMetricsBackend) {
this.activeJobsMetricWorkerService.createWorker(this.getWorkerProcessor(), this.getWorkerOptions());

this.activeJobsMetricWorkerService.bullMqWorker.on('completed', async (job) => {
Expand Down Expand Up @@ -95,26 +99,36 @@ export class ActiveJobsMetricService {
return await new Promise<void>(async (resolve, reject): Promise<void> => {
Logger.debug('metric job started', LOG_CONTEXT);
const deploymentName = process.env.FLEET_NAME ?? 'default';
let fatalError: unknown;

try {
for (const queueService of this.tokenList) {
for (const queueService of this.tokenList) {
try {
const waitCount = queueService.getGroupsJobsCount
? await queueService.getGroupsJobsCount()
: await queueService.getWaitingCount();
const delayedCount = await queueService.getDelayedCount();
const activeCount = await queueService.getActiveCount();

Logger.verbose('Recording active, waiting, and delayed metrics');
Logger.verbose(`Recording metrics for queue: ${queueService.topic}`);

this.metricsService.recordMetric(`Queue/${deploymentName}/${queueService.topic}/waiting`, waitCount);
this.metricsService.recordMetric(`Queue/${deploymentName}/${queueService.topic}/delayed`, delayedCount);
this.metricsService.recordMetric(`Queue/${deploymentName}/${queueService.topic}/active`, activeCount);
} catch (error) {
Logger.error(
error,
`Failed to collect metrics for queue: ${queueService.topic}`,
LOG_CONTEXT
);
fatalError = error;
}
}

return resolve();
} catch (error) {
return reject(error);
if (fatalError) {
return reject(fatalError);
}

return resolve();
});
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ export class AddJob {

if (tierValidationErrors && tierValidationErrors.length > 0) {
const errorMessage = tierValidationErrors[0].message;
this.logger.error(`${stepTypeName} duration exceeds tier limits: ${errorMessage}, jobId: ${job._id}`);
this.logger.debug(`${stepTypeName} duration exceeds tier limits: ${errorMessage}, jobId: ${job._id}`);

await this.createExecutionDetails.execute(
CreateExecutionDetailsCommand.create({
Expand All @@ -535,7 +535,7 @@ export class AddJob {
detail: DetailEnum
): Promise<AddJobResult> {
const stepTypeName = stepType.toLowerCase();
this.logger.error(`${stepTypeName} validation failed for job ${job._id}: ${error.message}`);
this.logger.debug(`${stepTypeName} validation failed for job ${job._id}: ${error.message}`);

await this.createExecutionDetails.execute(
CreateExecutionDetailsCommand.create({
Expand Down
3 changes: 3 additions & 0 deletions apps/worker/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const envValidators = {
PORT: port(),
STORE_ENCRYPTION_KEY: str32(),
STORE_NOTIFICATION_CONTENT: bool({ default: false }),
ENABLE_OTEL: bool({ default: false }),
ENABLE_OTEL_LOGS: bool({ default: false }),
OTEL_PROMETHEUS_PORT: num({ default: 9464 }),
MAX_NOVU_INTEGRATION_MAIL_REQUESTS: num({ default: 300 }),
NOVU_EMAIL_INTEGRATION_API_KEY: str({ default: '' }),
STORAGE_SERVICE: str({ default: undefined }),
Expand Down
16 changes: 14 additions & 2 deletions apps/worker/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import './config/env.config';

// Import from the tracing subpath, NOT the main barrel. The barrel loads
// @novu/application-generic which transitively pulls in pino/mongoose/ioredis.
// TypeScript hoists all imports — if pino loads before startOtel() registers
// instrumentations, PinoInstrumentation cannot patch the already-bound references.
// Importing only otel-init keeps those modules out of require.cache until after
// the SDK's require()-hooks are in place.
import { startOtel } from '@novu/application-generic/build/main/tracing/otel-init';
import { name, version } from '../package.json';

startOtel(name, version);

// biome-ignore lint: must execute after startOtel() so New Relic layers on top
require('newrelic');

import { init } from '@sentry/nestjs';
import { version } from '../package.json';
// biome-ignore lint: lazy require so @sentry/nestjs loads after OTEL instrumentations are installed
const { init } = require('@sentry/nestjs');

if (process.env.SENTRY_DSN) {
init({
Expand Down
20 changes: 17 additions & 3 deletions apps/ws/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import './config/env.config';
import 'newrelic';
import { init } from '@sentry/nestjs';
import { version } from '../package.json';

// Import from the tracing subpath, NOT the main barrel. The barrel loads
// @novu/application-generic which transitively pulls in pino/mongoose/ioredis.
// TypeScript hoists all imports — if pino loads before startOtel() registers
// instrumentations, PinoInstrumentation cannot patch the already-bound references.
// Importing only otel-init keeps those modules out of require.cache until after
// the SDK's require()-hooks are in place.
import { startOtel } from '@novu/application-generic/build/main/tracing/otel-init';
import { name, version } from '../package.json';

startOtel(name, version);

// biome-ignore lint: must execute after startOtel() so New Relic layers on top
require('newrelic');

// biome-ignore lint: lazy require so @sentry/nestjs loads after OTEL instrumentations are installed
const { init } = require('@sentry/nestjs');

if (process.env.SENTRY_DSN) {
init({
Expand Down
2 changes: 1 addition & 1 deletion enterprise/packages/translation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"sinon": "^9.2.4",
"ts-node": "~10.9.1",
"typescript": "5.6.2",
"liquidjs": "^10.20.1"
"liquidjs": "^10.25.0"
},
"peerDependencies": {
"@nestjs/common": "10.4.18",
Expand Down
Loading
Loading