feat(core): Add profiling support via sentry (#25049)

This commit is contained in:
Tomi Turtiainen 2026-01-30 11:49:50 +02:00 committed by GitHub
parent 779a04f471
commit 659de949cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 298 additions and 45 deletions

View file

@ -94,7 +94,7 @@
"@azure/identity": "4.13.0",
"@lezer/common": "^1.2.0",
"@mistralai/mistralai": "^1.10.0",
"@n8n/typeorm>@sentry/node": "catalog:",
"@n8n/typeorm>@sentry/node": "catalog:sentry",
"@types/node": "^20.17.50",
"axios": "1.12.0",
"chokidar": "4.0.3",

View file

@ -1,5 +1,10 @@
import { z } from 'zod';
import { Config, Env } from '../decorators';
/** Schema for sample rates (0.0 to 1.0). */
export const sampleRateSchema = z.number({ coerce: true }).min(0).max(1);
@Config
export class SentryConfig {
/** Sentry DSN (data source name) for the backend. */
@ -10,6 +15,26 @@ export class SentryConfig {
@Env('N8N_FRONTEND_SENTRY_DSN')
frontendDsn: string = '';
/**
* Sample rate for Sentry traces (0.0 to 1.0).
* This determines whether tracing is enabled and what percentage of
* transactions are traced.
*
* @default 0 (disabled)
*/
@Env('N8N_SENTRY_TRACES_SAMPLE_RATE', sampleRateSchema)
tracesSampleRate: number = 0;
/**
* Sample rate for Sentry profiling (0.0 to 1.0).
* This determines whether profiling is enabled and what percentage of
* transactions are profiled.
*
* @default 0 (disabled)
*/
@Env('N8N_SENTRY_PROFILES_SAMPLE_RATE', sampleRateSchema)
profilesSampleRate: number = 0;
/**
* Environment of the n8n instance.
*

View file

@ -43,6 +43,7 @@ export { Config, Env, Nested } from './decorators';
export { AiConfig } from './configs/ai.config';
export { DatabaseConfig, SqliteConfig } from './configs/database.config';
export { InstanceSettingsConfig } from './configs/instance-settings-config';
export { sampleRateSchema } from './configs/sentry.config';
export type { TaskRunnerMode } from './configs/runners.config';
export { TaskRunnersConfig } from './configs/runners.config';
export { SecurityConfig } from './configs/security.config';

View file

@ -281,6 +281,8 @@ describe('GlobalConfig', () => {
frontendDsn: '',
environment: '',
deploymentName: '',
profilesSampleRate: 0,
tracesSampleRate: 0,
},
logging: {
level: 'info',

View file

@ -22,7 +22,7 @@
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@sentry/node": "catalog:"
"@sentry/node": "catalog:sentry"
},
"dependencies": {
"callsites": "catalog:"

View file

@ -1,11 +1,13 @@
from dataclasses import dataclass
from src.env import read_str_env
from src.env import read_str_env, read_float_env
from src.constants import (
ENV_DEPLOYMENT_NAME,
ENV_ENVIRONMENT,
ENV_N8N_VERSION,
ENV_SENTRY_DSN,
ENV_SENTRY_PROFILES_SAMPLE_RATE,
ENV_SENTRY_TRACES_SAMPLE_RATE,
)
@ -15,6 +17,8 @@ class SentryConfig:
n8n_version: str
environment: str
deployment_name: str
profiles_sample_rate: float
traces_sample_rate: float
@property
def enabled(self) -> bool:
@ -27,4 +31,6 @@ class SentryConfig:
n8n_version=read_str_env(ENV_N8N_VERSION, ""),
environment=read_str_env(ENV_ENVIRONMENT, ""),
deployment_name=read_str_env(ENV_DEPLOYMENT_NAME, ""),
profiles_sample_rate=read_float_env(ENV_SENTRY_PROFILES_SAMPLE_RATE, 0),
traces_sample_rate=read_float_env(ENV_SENTRY_TRACES_SAMPLE_RATE, 0),
)

View file

@ -77,6 +77,8 @@ ENV_SENTRY_DSN = "N8N_SENTRY_DSN"
ENV_N8N_VERSION = "N8N_VERSION"
ENV_ENVIRONMENT = "ENVIRONMENT"
ENV_DEPLOYMENT_NAME = "DEPLOYMENT_NAME"
ENV_SENTRY_PROFILES_SAMPLE_RATE = "N8N_SENTRY_PROFILES_SAMPLE_RATE"
ENV_SENTRY_TRACES_SAMPLE_RATE = "N8N_SENTRY_TRACES_SAMPLE_RATE"
# Sentry
SENTRY_TAG_SERVER_TYPE_KEY = "server_type"

View file

@ -43,3 +43,15 @@ def read_bool_env(env_name: str, default: bool) -> bool:
if value is None:
return default
return value.strip().lower() == "true"
def read_float_env(env_name: str, default: float) -> float:
value = read_env(env_name)
if value is None:
return default
try:
return float(value)
except ValueError:
raise ValueError(
f"Environment variable {env_name} must be a float, got '{value}'"
)

View file

@ -20,18 +20,48 @@ class TaskRunnerSentry:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_sdk.init(
dsn=self.config.dsn,
release=f"n8n@{self.config.n8n_version}",
environment=self.config.environment,
server_name=self.config.deployment_name,
before_send=self._filter_out_ignored_errors,
attach_stacktrace=True,
send_default_pii=False,
auto_enabling_integrations=False,
default_integrations=True,
integrations=[LoggingIntegration(level=logging.ERROR)],
)
integrations = [LoggingIntegration(level=logging.ERROR)]
is_profiling_enabled = self.config.profiles_sample_rate > 0
if is_profiling_enabled:
try:
# Import profiling integration lazily to avoid hard dependency
import sentry_sdk.profiler # noqa: F401
self.logger.info("Sentry profiling integration loaded")
except ImportError:
self.logger.warning(
"Sentry profiling is enabled but sentry-sdk profiling is not available. "
"Install with: uv sync --all-extras"
)
is_tracing_enabled = self.config.traces_sample_rate > 0
if is_profiling_enabled and not is_tracing_enabled:
self.logger.warning(
"Profiling is enabled but tracing is disabled. Profiling will not work."
)
init_options = {
"dsn": self.config.dsn,
"release": f"n8n@{self.config.n8n_version}",
"environment": self.config.environment,
"server_name": self.config.deployment_name,
"before_send": self._filter_out_ignored_errors,
"attach_stacktrace": True,
"send_default_pii": False,
"auto_enabling_integrations": False,
"default_integrations": True,
"integrations": integrations,
}
if self.config.traces_sample_rate > 0:
init_options["traces_sample_rate"] = self.config.traces_sample_rate
if is_profiling_enabled:
init_options["profiles_sample_rate"] = self.config.profiles_sample_rate
sentry_sdk.init(**init_options)
sentry_sdk.set_tag(SENTRY_TAG_SERVER_TYPE_KEY, SENTRY_TAG_SERVER_TYPE_VALUE)
self.logger.info("Sentry ready")

View file

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from unittest.mock import patch
from src.env import read_env, read_int_env, read_bool_env, read_str_env
from src.env import read_env, read_int_env, read_bool_env, read_str_env, read_float_env
class TestReadEnv:
@ -179,3 +179,45 @@ class TestReadBoolEnv:
with patch.dict(os.environ, clear=True):
result = read_bool_env("TEST_BOOL", default=True)
assert result is True
class TestReadFloatEnv:
def test_returns_float_from_direct_env(self):
with patch.dict(os.environ, {"TEST_FLOAT": "3.14"}):
result = read_float_env("TEST_FLOAT", default=0.0)
assert result == 3.14
def test_returns_float_from_file(self):
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
f.write("2.718")
f.flush()
with patch.dict(os.environ, {"TEST_FLOAT_FILE": f.name}):
result = read_float_env("TEST_FLOAT", default=0.0)
assert result == 2.718
def test_returns_default_when_not_set(self):
with patch.dict(os.environ, clear=True):
result = read_float_env("TEST_FLOAT", default=9.99)
assert result == 9.99
def test_raises_error_for_invalid_float(self):
with patch.dict(os.environ, {"TEST_FLOAT": "not_a_number"}):
with pytest.raises(ValueError) as exc_info:
read_float_env("TEST_FLOAT", default=0.0)
assert "must be a float" in str(exc_info.value)
def test_handles_negative_numbers(self):
with patch.dict(os.environ, {"TEST_FLOAT": "-42.5"}):
result = read_float_env("TEST_FLOAT", default=0.0)
assert result == -42.5
def test_handles_integer_values(self):
with patch.dict(os.environ, {"TEST_FLOAT": "42"}):
result = read_float_env("TEST_FLOAT", default=0.0)
assert result == 42.0
def test_handles_zero(self):
with patch.dict(os.environ, {"TEST_FLOAT": "0"}):
result = read_float_env("TEST_FLOAT", default=1.0)
assert result == 0.0

View file

@ -24,6 +24,8 @@ def sentry_config():
n8n_version="1.0.0",
environment="test",
deployment_name="test-deployment",
profiles_sample_rate=0,
traces_sample_rate=0,
)
@ -34,6 +36,8 @@ def disabled_sentry_config():
n8n_version="1.0.0",
environment="test",
deployment_name="test-deployment",
profiles_sample_rate=0,
traces_sample_rate=0,
)
@ -236,6 +240,8 @@ class TestSentryConfig:
"N8N_VERSION": "2.0.0",
"ENVIRONMENT": "production",
"DEPLOYMENT_NAME": "prod-deployment",
"N8N_SENTRY_PROFILES_SAMPLE_RATE": "0.5",
"N8N_SENTRY_TRACES_SAMPLE_RATE": "0.1",
},
)
def test_from_env_creates_config_from_environment(self):
@ -245,6 +251,8 @@ class TestSentryConfig:
assert config.n8n_version == "2.0.0"
assert config.environment == "production"
assert config.deployment_name == "prod-deployment"
assert config.profiles_sample_rate == 0.5
assert config.traces_sample_rate == 0.1
@patch.dict("os.environ", {}, clear=True)
def test_from_env_uses_defaults_when_missing(self):
@ -254,3 +262,5 @@ class TestSentryConfig:
assert config.n8n_version == ""
assert config.environment == ""
assert config.deployment_name == ""
assert config.profiles_sample_rate == 0
assert config.traces_sample_rate == 0

View file

@ -39,7 +39,7 @@
"@n8n/config": "workspace:*",
"@n8n/di": "workspace:*",
"@n8n/errors": "workspace:*",
"@sentry/node": "catalog:",
"@sentry/node": "catalog:sentry",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"lodash": "catalog:",

View file

@ -9,6 +9,8 @@ describe('TaskRunnerSentry', () => {
n8nVersion: '1.0.0',
environment: 'local',
deploymentName: 'test',
profilesSampleRate: 0,
tracesSampleRate: 0,
};
afterEach(() => {
@ -258,6 +260,11 @@ describe('TaskRunnerSentry', () => {
serverName: 'test',
serverType: 'task_runner',
withEventLoopBlockDetection: false,
profilesSampleRate: 0,
tracesSampleRate: 0,
eligibleIntegrations: {
Http: true,
},
});
});
});

View file

@ -1,4 +1,4 @@
import { Config, Env } from '@n8n/config';
import { Config, Env, sampleRateSchema } from '@n8n/config';
@Config
export class SentryConfig {
@ -6,6 +6,24 @@ export class SentryConfig {
@Env('N8N_SENTRY_DSN')
dsn: string = '';
/**
* Sample rate for Sentry profiling (0.0 to 1.0).
* This determines what percentage of transactions are profiled.
*
* @default 0 (disabled)
*/
@Env('N8N_SENTRY_PROFILES_SAMPLE_RATE', sampleRateSchema)
profilesSampleRate: number = 0;
/**
* Sample rate for Sentry traces (0.0 to 1.0).
* This determines what percentage of transactions are profiled.
*
* @default 0 (disabled)
*/
@Env('N8N_SENTRY_TRACES_SAMPLE_RATE', sampleRateSchema)
tracesSampleRate: number = 0;
//#region Metadata about the environment
@Env('N8N_VERSION')

View file

@ -62,6 +62,8 @@ describe('JsTaskRunner', () => {
deploymentName: '',
environment: '',
n8nVersion: '',
profilesSampleRate: 0,
tracesSampleRate: 0,
},
});

View file

@ -15,7 +15,8 @@ export class TaskRunnerSentry {
) {}
async initIfEnabled() {
const { dsn, n8nVersion, environment, deploymentName } = this.config;
const { dsn, n8nVersion, environment, deploymentName, profilesSampleRate, tracesSampleRate } =
this.config;
if (!dsn) return;
@ -27,6 +28,11 @@ export class TaskRunnerSentry {
serverName: deploymentName,
beforeSendFilter: this.filterOutUserCodeErrors,
withEventLoopBlockDetection: false,
tracesSampleRate,
profilesSampleRate,
eligibleIntegrations: {
Http: true,
},
});
}

View file

@ -115,7 +115,7 @@
"@n8n_io/license-sdk": "2.25.0",
"@parcel/watcher": "^2.5.1",
"@rudderstack/rudder-sdk-node": "3.0.0",
"@sentry/node": "catalog:",
"@sentry/node": "catalog:sentry",
"aws4": "1.11.0",
"axios": "catalog:",
"bcryptjs": "2.4.3",

View file

@ -84,7 +84,8 @@ export abstract class BaseCommand<F = never> {
this.dbConnection = Container.get(DbConnection);
this.errorReporter = Container.get(ErrorReporter);
const { backendDsn, environment, deploymentName } = this.globalConfig.sentry;
const { backendDsn, environment, deploymentName, profilesSampleRate, tracesSampleRate } =
this.globalConfig.sentry;
await this.errorReporter.init({
serverType: this.instanceSettings.instanceType,
dsn: backendDsn,
@ -93,6 +94,16 @@ export abstract class BaseCommand<F = never> {
serverName: deploymentName,
releaseDate: N8N_RELEASE_DATE,
withEventLoopBlockDetection: true,
tracesSampleRate,
profilesSampleRate,
eligibleIntegrations: {
Express: true,
Http: true,
Postgres: this.globalConfig.database.type === 'postgresdb',
Redis:
this.globalConfig.executions.mode === 'queue' ||
this.globalConfig.cache.backend === 'redis',
},
});
process.once('SIGTERM', this.onTerminationSignal('SIGTERM'));

View file

@ -47,8 +47,9 @@
"@n8n/constants": "workspace:*",
"@n8n/decorators": "workspace:*",
"@n8n/di": "workspace:*",
"@sentry/node": "catalog:",
"@sentry/node-native": "^10.36.0",
"@sentry/node": "catalog:sentry",
"@sentry/node-native": "catalog:sentry",
"@sentry/profiling-node": "catalog:sentry",
"axios": "catalog:",
"callsites": "catalog:",
"chardet": "2.0.0",

View file

@ -1,4 +1,4 @@
import { inProduction, inTest, Logger } from '@n8n/backend-common';
import { inTest, Logger } from '@n8n/backend-common';
import { type InstanceType } from '@n8n/constants';
import { Service } from '@n8n/di';
import type { ReportingOptions } from '@n8n/errors';
@ -8,6 +8,8 @@ import { AxiosError } from 'axios';
import { ApplicationError, ExecutionCancelledError, BaseError } from 'n8n-workflow';
import { createHash } from 'node:crypto';
type SentryIntegration = 'Redis' | 'Postgres' | 'Http' | 'Express';
type ErrorReporterInitOptions = {
serverType: InstanceType | 'task_runner';
dsn: string;
@ -19,11 +21,23 @@ type ErrorReporterInitOptions = {
/** Whether to enable event loop block detection, if Sentry is enabled. */
withEventLoopBlockDetection: boolean;
/** Sample rate for Sentry traces (0.0 to 1.0). 0 means disabled */
tracesSampleRate: number;
/** Sample rate for Sentry profiling (0.0 to 1.0). 0 means disabled */
profilesSampleRate: number;
/**
* Function to allow filtering out errors before they are sent to Sentry.
* Return true if the error should be filtered out.
*/
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
/**
* Integrations eligible for enablement. `tracesSampleRate` still determines
* whether they are actually enabled or not.
*/
eligibleIntegrations?: Partial<Record<SentryIntegration, boolean>>;
};
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
@ -88,6 +102,9 @@ export class ErrorReporter {
serverName,
releaseDate,
withEventLoopBlockDetection,
profilesSampleRate,
tracesSampleRate,
eligibleIntegrations = {},
}: ErrorReporterInitOptions) {
if (inTest) return;
@ -123,13 +140,28 @@ export class ErrorReporter {
const { init, captureException, setTag } = await import('@sentry/node');
const { requestDataIntegration, rewriteFramesIntegration } = await import('@sentry/node');
const enabledIntegrations = [
// Most of the integrations are listed here:
// https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/
const enabledIntegrations = new Set([
'InboundFilters',
'FunctionToString',
'LinkedErrors',
'OnUnhandledRejection',
'ContextLines',
];
]);
const isTracingEnabled = tracesSampleRate > 0;
if (isTracingEnabled) {
const tracingIntegrations: SentryIntegration[] = ['Http', 'Postgres', 'Redis', 'Express'];
tracingIntegrations
.filter((integrationName) => !!eligibleIntegrations[integrationName])
.forEach((integrationName) => enabledIntegrations.add(integrationName));
}
const isProfilingEnabled = profilesSampleRate > 0;
if (isProfilingEnabled && !isTracingEnabled) {
this.logger.warn('Profiling is enabled but tracing is disabled. Profiling will not work.');
}
const eventLoopBlockIntegration = withEventLoopBlockDetection
? // The EventLoopBlockIntegration doesn't automatically include the
@ -140,16 +172,18 @@ export class ErrorReporter {
})
: [];
const profilingIntegration = isProfilingEnabled ? await this.getProfilingIntegration() : [];
init({
dsn,
release,
environment,
tracesSampleRate: inProduction ? 0.01 : 0,
serverName,
beforeBreadcrumb: () => null,
...(isTracingEnabled ? { tracesSampleRate } : {}),
...(isProfilingEnabled ? { profilesSampleRate, profileLifecycle: 'trace' } : {}),
beforeSend: this.beforeSend.bind(this) as NodeOptions['beforeSend'],
integrations: (integrations) => [
...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
...integrations.filter(({ name }) => enabledIntegrations.has(name)),
rewriteFramesIntegration({ root: '/' }),
requestDataIntegration({
include: {
@ -161,6 +195,7 @@ export class ErrorReporter {
},
}),
...eventLoopBlockIntegration,
...profilingIntegration,
],
});
@ -276,10 +311,22 @@ export class ErrorReporter {
}),
];
} catch {
this.logger.debug(
this.logger.warn(
"Sentry's event loop block integration is disabled, because the native binary for `@sentry/node-native` was not found",
);
return [];
}
}
private async getProfilingIntegration() {
try {
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
return [nodeProfilingIntegration()];
} catch {
this.logger.warn(
'Sentry profiling is disabled, because the `@sentry/profiling-node` package was not found',
);
return [];
}
}
}

View file

@ -66,9 +66,6 @@ catalogs:
'@n8n_io/ai-assistant-sdk':
specifier: 1.20.0
version: 1.20.0
'@sentry/node':
specifier: ^10.36.0
version: 10.36.0
'@types/basic-auth':
specifier: ^1.1.3
version: 1.1.3
@ -251,6 +248,16 @@ catalogs:
vue-router:
specifier: ^4.5.0
version: 4.5.0
sentry:
'@sentry/node':
specifier: ^10.36.0
version: 10.36.0
'@sentry/node-native':
specifier: ^10.36.0
version: 10.36.0
'@sentry/profiling-node':
specifier: ^10.36.0
version: 10.36.0
storybook:
'@chromatic-com/storybook':
specifier: ^4.1.3
@ -958,7 +965,7 @@ importers:
specifier: workspace:*
version: link:../typescript-config
'@sentry/node':
specifier: 'catalog:'
specifier: catalog:sentry
version: 10.36.0
packages/@n8n/eslint-config:
@ -1579,7 +1586,7 @@ importers:
specifier: workspace:*
version: link:../errors
'@sentry/node':
specifier: 'catalog:'
specifier: catalog:sentry
version: 10.36.0
acorn:
specifier: 8.14.0
@ -1739,7 +1746,7 @@ importers:
specifier: 3.0.0
version: 3.0.0
'@sentry/node':
specifier: 'catalog:'
specifier: catalog:sentry
version: 10.36.0
aws4:
specifier: 1.11.0
@ -2091,10 +2098,13 @@ importers:
specifier: workspace:*
version: link:../@n8n/di
'@sentry/node':
specifier: 'catalog:'
specifier: catalog:sentry
version: 10.36.0
'@sentry/node-native':
specifier: ^10.36.0
specifier: catalog:sentry
version: 10.36.0
'@sentry/profiling-node':
specifier: catalog:sentry
version: 10.36.0
axios:
specifier: 1.12.0
@ -7462,6 +7472,10 @@ packages:
resolution: {integrity: sha512-zPjz7AbcxEyx8AHj8xvp28fYtPTPWU1XcNtymhAHJLS9CXOblqSC7W02Jxz6eo3eR1/pLyOo6kJBUjvLe9EoFA==}
engines: {node: '>=18'}
'@sentry-internal/node-cpu-profiler@2.2.0':
resolution: {integrity: sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ==}
engines: {node: '>=18'}
'@sentry-internal/node-native-stacktrace@0.3.0':
resolution: {integrity: sha512-ef0M2y2JDrC/H0AxMJJQInGTdZTlnwa6AAVWR4fMOpJRubkfdH2IZXE/nWU0Nj74oeJLQgdPtS6DeijLJtqq8Q==}
engines: {node: '>=18'}
@ -7572,6 +7586,11 @@ packages:
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0
'@opentelemetry/semantic-conventions': ^1.37.0
'@sentry/profiling-node@10.36.0':
resolution: {integrity: sha512-lKaFPFN/XgX2LjUstSPWhp0xyifMC71MG8OFbgpy23n1Nnl5TExZehm7Ppaoad/Rhc2cfF3WJByOKgsHasX1Rg==}
engines: {node: '>=18'}
hasBin: true
'@sentry/vite-plugin@4.3.0':
resolution: {integrity: sha512-MeTAHMmTOgBPMAjeW7/ONyXwgScZdaFFtNiALKcAODnVqC7eoHdSRIWeH5mkLr2Dvs7nqtBaDpKxRjUBgfm9LQ==}
engines: {node: '>= 14'}
@ -14655,9 +14674,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
module-details-from-path@1.0.3:
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
@ -23983,6 +23999,11 @@ snapshots:
dependencies:
'@sentry/core': 10.36.0
'@sentry-internal/node-cpu-profiler@2.2.0':
dependencies:
detect-libc: 2.1.2
node-abi: 3.75.0
'@sentry-internal/node-native-stacktrace@0.3.0':
dependencies:
detect-libc: 2.1.2
@ -24141,6 +24162,14 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.39.0
'@sentry/core': 10.36.0
'@sentry/profiling-node@10.36.0':
dependencies:
'@sentry-internal/node-cpu-profiler': 2.2.0
'@sentry/core': 10.36.0
'@sentry/node': 10.36.0
transitivePeerDependencies:
- supports-color
'@sentry/vite-plugin@4.3.0(encoding@0.1.13)':
dependencies:
'@sentry/bundler-plugin-core': 4.3.0(encoding@0.1.13)
@ -25451,7 +25480,7 @@ snapshots:
'@ts-morph/common@0.27.0':
dependencies:
fast-glob: 3.3.3
minimatch: 10.0.3
minimatch: 10.1.1
path-browserify: 1.0.1
'@tsconfig/node10@1.0.11':
@ -33234,8 +33263,6 @@ snapshots:
ast-module-types: 6.0.1
node-source-walk: 7.0.1
module-details-from-path@1.0.3: {}
module-details-from-path@1.0.4: {}
module-lookup-amd@9.0.5:
@ -35046,7 +35073,7 @@ snapshots:
require-in-the-middle@8.0.1:
dependencies:
debug: 4.4.3(supports-color@8.1.1)
module-details-from-path: 1.0.3
module-details-from-path: 1.0.4
transitivePeerDependencies:
- supports-color

View file

@ -115,6 +115,10 @@ catalogs:
eslint-plugin-playwright: 2.2.2
playwright: 1.57.0
playwright-core: 1.57.0
sentry:
'@sentry/node': '^10.36.0'
'@sentry/node-native': '^10.36.0'
'@sentry/profiling-node': '^10.36.0'
minimumReleaseAge: 4320