mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
chore(core): Stop reporting errors to Sentry for older releases (no-changelog) (#13323)
This commit is contained in:
parent
aae55fe7ac
commit
ac1f651905
1
.github/workflows/release-publish.yml
vendored
1
.github/workflows/release-publish.yml
vendored
|
@ -103,6 +103,7 @@ jobs:
|
||||||
context: ./docker/images/n8n
|
context: ./docker/images/n8n
|
||||||
build-args: |
|
build-args: |
|
||||||
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
||||||
|
N8N_RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
provenance: false
|
provenance: false
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -4,15 +4,18 @@ FROM n8nio/base:${NODE_VERSION}
|
||||||
ARG N8N_VERSION
|
ARG N8N_VERSION
|
||||||
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
||||||
|
|
||||||
|
ARG N8N_RELEASE_DATE
|
||||||
LABEL org.opencontainers.image.title="n8n"
|
LABEL org.opencontainers.image.title="n8n"
|
||||||
LABEL org.opencontainers.image.description="Workflow Automation Tool"
|
LABEL org.opencontainers.image.description="Workflow Automation Tool"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
|
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
|
||||||
LABEL org.opencontainers.image.url="https://n8n.io"
|
LABEL org.opencontainers.image.url="https://n8n.io"
|
||||||
LABEL org.opencontainers.image.version=${N8N_VERSION}
|
LABEL org.opencontainers.image.version=${N8N_VERSION}
|
||||||
|
LABEL org.opencontainers.image.created=${N8N_RELEASE_DATE}
|
||||||
|
|
||||||
ENV N8N_VERSION=${N8N_VERSION}
|
ENV N8N_VERSION=${N8N_VERSION}
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV N8N_RELEASE_TYPE=stable
|
ENV N8N_RELEASE_TYPE=stable
|
||||||
|
ENV N8N_RELEASE_DATE=${N8N_RELEASE_DATE}
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
|
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
|
||||||
npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \
|
npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \
|
||||||
|
|
|
@ -9,6 +9,9 @@ export class GenericConfig {
|
||||||
@Env('N8N_RELEASE_TYPE')
|
@Env('N8N_RELEASE_TYPE')
|
||||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
||||||
|
|
||||||
|
@Env('N8N_RELEASE_DATE')
|
||||||
|
releaseDate?: Date;
|
||||||
|
|
||||||
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
||||||
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
||||||
gracefulShutdownTimeout: number = 30;
|
gracefulShutdownTimeout: number = 30;
|
||||||
|
|
|
@ -55,6 +55,13 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Invalid boolean value for ${envName}: ${value}`);
|
console.warn(`Invalid boolean value for ${envName}: ${value}`);
|
||||||
}
|
}
|
||||||
|
} else if (type === Date) {
|
||||||
|
const timestamp = Date.parse(value);
|
||||||
|
if (isNaN(timestamp)) {
|
||||||
|
console.warn(`Invalid timestamp value for ${envName}: ${value}`);
|
||||||
|
} else {
|
||||||
|
config[key] = new Date(timestamp);
|
||||||
|
}
|
||||||
} else if (type === String) {
|
} else if (type === String) {
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,9 +8,12 @@ jest.mock('fs');
|
||||||
const mockFs = mock<typeof fs>();
|
const mockFs = mock<typeof fs>();
|
||||||
fs.readFileSync = mockFs.readFileSync;
|
fs.readFileSync = mockFs.readFileSync;
|
||||||
|
|
||||||
|
const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
describe('GlobalConfig', () => {
|
describe('GlobalConfig', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Container.reset();
|
Container.reset();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
|
@ -18,10 +21,6 @@ describe('GlobalConfig', () => {
|
||||||
process.env = originalEnv;
|
process.env = originalEnv;
|
||||||
});
|
});
|
||||||
|
|
||||||
// deepCopy for diff to show plain objects
|
|
||||||
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
|
|
||||||
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
|
||||||
|
|
||||||
const defaultConfig: GlobalConfig = {
|
const defaultConfig: GlobalConfig = {
|
||||||
path: '/',
|
path: '/',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
@ -314,7 +313,7 @@ describe('GlobalConfig', () => {
|
||||||
it('should use all default values when no env variables are defined', () => {
|
it('should use all default values when no env variables are defined', () => {
|
||||||
process.env = {};
|
process.env = {};
|
||||||
const config = Container.get(GlobalConfig);
|
const config = Container.get(GlobalConfig);
|
||||||
expect(deepCopy(config)).toEqual(defaultConfig);
|
expect(structuredClone(config)).toEqual(defaultConfig);
|
||||||
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -327,9 +326,10 @@ describe('GlobalConfig', () => {
|
||||||
DB_LOGGING_MAX_EXECUTION_TIME: '0',
|
DB_LOGGING_MAX_EXECUTION_TIME: '0',
|
||||||
N8N_METRICS: 'TRUE',
|
N8N_METRICS: 'TRUE',
|
||||||
N8N_TEMPLATES_ENABLED: '0',
|
N8N_TEMPLATES_ENABLED: '0',
|
||||||
|
N8N_RELEASE_DATE: '2025-02-17T13:54:15Z',
|
||||||
};
|
};
|
||||||
const config = Container.get(GlobalConfig);
|
const config = Container.get(GlobalConfig);
|
||||||
expect(deepCopy(config)).toEqual({
|
expect(structuredClone(config)).toEqual({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
database: {
|
database: {
|
||||||
logging: defaultConfig.database.logging,
|
logging: defaultConfig.database.logging,
|
||||||
|
@ -358,6 +358,10 @@ describe('GlobalConfig', () => {
|
||||||
...defaultConfig.templates,
|
...defaultConfig.templates,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
generic: {
|
||||||
|
...defaultConfig.generic,
|
||||||
|
releaseDate: new Date('2025-02-17T13:54:15.000Z'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -370,7 +374,7 @@ describe('GlobalConfig', () => {
|
||||||
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');
|
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');
|
||||||
|
|
||||||
const config = Container.get(GlobalConfig);
|
const config = Container.get(GlobalConfig);
|
||||||
expect(deepCopy(config)).toEqual({
|
expect(structuredClone(config)).toEqual({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
database: {
|
database: {
|
||||||
...defaultConfig.database,
|
...defaultConfig.database,
|
||||||
|
@ -382,4 +386,26 @@ describe('GlobalConfig', () => {
|
||||||
});
|
});
|
||||||
expect(mockFs.readFileSync).toHaveBeenCalled();
|
expect(mockFs.readFileSync).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle invalid numbers', () => {
|
||||||
|
process.env = {
|
||||||
|
DB_LOGGING_MAX_EXECUTION_TIME: 'abcd',
|
||||||
|
};
|
||||||
|
const config = Container.get(GlobalConfig);
|
||||||
|
expect(config.database.logging.maxQueryExecutionTime).toEqual(0);
|
||||||
|
expect(consoleWarnMock).toHaveBeenCalledWith(
|
||||||
|
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid timestamps', () => {
|
||||||
|
process.env = {
|
||||||
|
N8N_RELEASE_DATE: 'abcd',
|
||||||
|
};
|
||||||
|
const config = Container.get(GlobalConfig);
|
||||||
|
expect(config.generic.releaseDate).toBeUndefined();
|
||||||
|
expect(consoleWarnMock).toHaveBeenCalledWith(
|
||||||
|
'Invalid timestamp value for N8N_RELEASE_DATE: abcd',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -63,6 +63,7 @@ export abstract class BaseCommand extends Command {
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
this.errorReporter = Container.get(ErrorReporter);
|
this.errorReporter = Container.get(ErrorReporter);
|
||||||
|
|
||||||
|
const { releaseDate } = this.globalConfig.generic;
|
||||||
const { backendDsn, n8nVersion, environment, deploymentName } = this.globalConfig.sentry;
|
const { backendDsn, n8nVersion, environment, deploymentName } = this.globalConfig.sentry;
|
||||||
await this.errorReporter.init({
|
await this.errorReporter.init({
|
||||||
serverType: this.instanceSettings.instanceType,
|
serverType: this.instanceSettings.instanceType,
|
||||||
|
@ -70,6 +71,7 @@ export abstract class BaseCommand extends Command {
|
||||||
environment,
|
environment,
|
||||||
release: n8nVersion,
|
release: n8nVersion,
|
||||||
serverName: deploymentName,
|
serverName: deploymentName,
|
||||||
|
releaseDate,
|
||||||
});
|
});
|
||||||
initExpressionEvaluator();
|
initExpressionEvaluator();
|
||||||
|
|
||||||
|
@ -294,6 +296,8 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
await this.shutdownService.waitForShutdown();
|
await this.shutdownService.waitForShutdown();
|
||||||
|
|
||||||
|
await this.errorReporter.shutdown();
|
||||||
|
|
||||||
await this.stopProcess();
|
await this.stopProcess();
|
||||||
|
|
||||||
clearTimeout(forceShutdownTimer);
|
clearTimeout(forceShutdownTimer);
|
||||||
|
|
|
@ -16,6 +16,7 @@ type ErrorReporterInitOptions = {
|
||||||
release: string;
|
release: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
serverName: string;
|
serverName: string;
|
||||||
|
releaseDate?: Date;
|
||||||
/**
|
/**
|
||||||
* Function to allow filtering out errors before they are sent to Sentry.
|
* Function to allow filtering out errors before they are sent to Sentry.
|
||||||
* Return true if the error should be filtered out.
|
* Return true if the error should be filtered out.
|
||||||
|
@ -23,8 +24,14 @@ type ErrorReporterInitOptions = {
|
||||||
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SIX_WEEKS_IN_MS = 6 * 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const RELEASE_EXPIRATION_WARNING =
|
||||||
|
'Error tracking disabled because this release is older than 6 weeks.';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ErrorReporter {
|
export class ErrorReporter {
|
||||||
|
private expirationTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
/** Hashes of error stack traces, to deduplicate error reports. */
|
/** Hashes of error stack traces, to deduplicate error reports. */
|
||||||
private seenErrors = new Set<string>();
|
private seenErrors = new Set<string>();
|
||||||
|
|
||||||
|
@ -61,6 +68,7 @@ export class ErrorReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(timeoutInMs = 1000) {
|
async shutdown(timeoutInMs = 1000) {
|
||||||
|
clearTimeout(this.expirationTimer);
|
||||||
await close(timeoutInMs);
|
await close(timeoutInMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,11 +79,26 @@ export class ErrorReporter {
|
||||||
release,
|
release,
|
||||||
environment,
|
environment,
|
||||||
serverName,
|
serverName,
|
||||||
|
releaseDate,
|
||||||
}: ErrorReporterInitOptions) {
|
}: ErrorReporterInitOptions) {
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
this.error(error);
|
this.error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (releaseDate) {
|
||||||
|
const releaseExpiresInMs = releaseDate.getTime() + SIX_WEEKS_IN_MS - Date.now();
|
||||||
|
if (releaseExpiresInMs <= 0) {
|
||||||
|
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Once this release expires, reject all events
|
||||||
|
this.expirationTimer = setTimeout(() => {
|
||||||
|
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
this.report = this.defaultReport;
|
||||||
|
}, releaseExpiresInMs);
|
||||||
|
}
|
||||||
|
|
||||||
if (!dsn) return;
|
if (!dsn) return;
|
||||||
|
|
||||||
// Collect longer stacktraces
|
// Collect longer stacktraces
|
||||||
|
|
Loading…
Reference in a new issue