mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(core): Include reportTime and lastSuccessfulReport of instance-reporting in module-settings endpoint (no-changelog) (backport to release-candidate/2.39.x) (#38632)
Co-authored-by: Konstantin Tieber <46342664+konstantintieber@users.noreply.github.com>
This commit is contained in:
parent
c3dcbbef45
commit
bb72aa27ee
|
|
@ -336,6 +336,17 @@ export type FrontendModuleSettings = {
|
|||
autoExposeNewWorkflows: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client settings for the instance-reporting module. Present only when the
|
||||
* module is enabled on this instance.
|
||||
*/
|
||||
'instance-reporting'?: {
|
||||
/** Whether a receiver is configured, i.e. whether reports are actually sent. */
|
||||
enabled: boolean;
|
||||
/** Minute of the UTC day the daily report fires at, as `HH:mm`. Absent when disabled. */
|
||||
reportTime?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client settings for the encryption-key-manager module.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type * from './community-node-types';
|
|||
export type * from './quick-connect';
|
||||
export * from './agents/index';
|
||||
export * from './instance-registry-types';
|
||||
export type * from './instance-reporting';
|
||||
export type * from './worker-pools';
|
||||
export * from './redaction-enforcement';
|
||||
export * from './redaction-enforcement-floor';
|
||||
|
|
|
|||
7
packages/@n8n/api-types/src/instance-reporting.ts
Normal file
7
packages/@n8n/api-types/src/instance-reporting.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export type InstanceReportingStatus = {
|
||||
/**
|
||||
* When the receiver last accepted a report, as an ISO 8601 UTC string, or
|
||||
* `null` when it never did.
|
||||
*/
|
||||
lastSuccessfulReport: string | null;
|
||||
};
|
||||
|
|
@ -419,7 +419,7 @@ export default defineConfig(
|
|||
'./src/license.ts',
|
||||
'./src/modules/agents/integrations/n8n-checkpoint-storage.ts',
|
||||
'./src/modules/instance-ai/instance-ai.service.ts',
|
||||
'./src/modules/instance-reporting/instance-reporting-scheduler.service.ts',
|
||||
'./src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts',
|
||||
'./src/services/pruning/executions-pruning.service.ts',
|
||||
'./src/services/pruning/workflow-history-compaction.service.ts',
|
||||
'./src/services/workflow-statistics-rollup.service.ts',
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ there.
|
|||
|
||||
| Env var | Default | Notes |
|
||||
|---|---|---|
|
||||
| `N8N_INSTANCE_REPORTING_BASE_URL` | `''` | Base URL of the receiver. The report is POSTed to `<base>/api/v1/instance-reports`. Left unset, the module loads but warns and never sends. |
|
||||
| `N8N_INSTANCE_REPORTING_BASE_URL` | `''` | Base URL of the receiver. The report is POSTed to `<base>/api/v1/instance-reports`. Left unset, the module loads but warns and never sends: it starts no scheduler and claims no report time. |
|
||||
| `N8N_INSTANCE_REPORTING_LABEL` | `''` | Sent as `label` in the payload, when set. |
|
||||
| `N8N_INSTANCE_REPORTING_AUTH_TOKEN` | `''` | Sent as `Authorization: Bearer …`, when set. |
|
||||
|
||||
|
|
@ -99,5 +99,38 @@ instance calling at the same minute. The time is always UTC and never before
|
|||
first; it shifts itself later, logging a warning, if
|
||||
`N8N_INSIGHTS_COMPACTION_INTERVAL_MINUTES` is raised enough to require it.
|
||||
|
||||
## Client settings
|
||||
|
||||
`GET /rest/module-settings` carries an `instance-reporting` key when the module
|
||||
is enabled on this instance:
|
||||
|
||||
```json
|
||||
{
|
||||
"instance-reporting": {
|
||||
"enabled": true,
|
||||
"reportTime": "07:42"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`enabled` says whether a receiver is configured. Without one the key reads
|
||||
`{ "enabled": false }` and carries no `reportTime`, since no time is claimed. A
|
||||
missing key means the module is not enabled at all.
|
||||
|
||||
These settings are built once, during module init, and served from a cache
|
||||
afterwards, so they carry only values that stay the same for the lifetime of
|
||||
the process.
|
||||
|
||||
## Reporting status
|
||||
|
||||
`GET /rest/instance-reporting/status` answers with the UTC instant the receiver
|
||||
last accepted a report, or `null` when it never did:
|
||||
|
||||
```json
|
||||
{ "lastSuccessfulReport": "2026-03-25T07:42:13.000Z" }
|
||||
```
|
||||
|
||||
The route exists whenever the module is loaded, including without a receiver — the client decides whether to ask by reading `enabled` from the client settings.
|
||||
|
||||
See [.agents/specs/central-instance-monitoring.md](../../../../../.agents/specs/central-instance-monitoring.md)
|
||||
for the full design.
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import type { ModuleName, ModulesConfig } from '@n8n/backend-common';
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import { Time } from '@n8n/constants';
|
||||
import type { InstanceSettings } from 'n8n-core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import type { Mocked } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
|
|
@ -11,7 +9,6 @@ import type { EventService } from '@/events/event.service';
|
|||
import type { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
|
||||
import { InstanceReportingScheduler } from '../instance-reporting-scheduler.service';
|
||||
import type { InstanceReportingSettingsService } from '../instance-reporting-settings.service';
|
||||
import { InstanceReportingConfig } from '../instance-reporting.config';
|
||||
import type { InstanceReportingService } from '../instance-reporting.service';
|
||||
|
||||
const REPORT_TIME = '07:42';
|
||||
|
|
@ -28,24 +25,18 @@ interface Harness {
|
|||
reportRepository: Mocked<InstanceMonitoringReportRepository>;
|
||||
settingsService: Mocked<InstanceReportingSettingsService>;
|
||||
instanceSettings: Mocked<InstanceSettings>;
|
||||
modulesConfig: Mocked<ModulesConfig>;
|
||||
eventService: Mocked<EventService>;
|
||||
/** Fire the deferred `server-started` listeners (only needed when autoStart is false). */
|
||||
startServer: () => void;
|
||||
}
|
||||
|
||||
function makeHarness({
|
||||
baseUrl = 'https://example.com',
|
||||
isLeader = true,
|
||||
disabledModules = [] as ModuleName[],
|
||||
// The scheduler arms its first tick on `server-started`. By default the harness
|
||||
// fires that listener the moment it is registered, so `init()` starts reporting
|
||||
// as the tests expect. Pass false to hold it and fire it later via `startServer`.
|
||||
autoStart = true,
|
||||
} = {}): Harness {
|
||||
const config = new InstanceReportingConfig();
|
||||
config.instanceReportingBaseUrl = baseUrl;
|
||||
|
||||
const reportingService = mock<InstanceReportingService>();
|
||||
// No attempt has been made yet, so nothing is holding the next one back.
|
||||
reportingService.msUntilRetryAllowed.mockResolvedValue(0);
|
||||
|
|
@ -61,7 +52,6 @@ function makeHarness({
|
|||
instanceRole: isLeader ? 'leader' : 'follower',
|
||||
isLeader,
|
||||
});
|
||||
const modulesConfig = mock<ModulesConfig>({ disabledModules });
|
||||
|
||||
const serverStartedListeners: Array<() => void> = [];
|
||||
const eventService = mock<EventService>();
|
||||
|
|
@ -81,12 +71,10 @@ function makeHarness({
|
|||
});
|
||||
|
||||
const scheduler = new InstanceReportingScheduler(
|
||||
config,
|
||||
reportingService,
|
||||
reportRepository,
|
||||
settingsService,
|
||||
instanceSettings,
|
||||
modulesConfig,
|
||||
eventService,
|
||||
mockLogger(),
|
||||
);
|
||||
|
|
@ -97,7 +85,6 @@ function makeHarness({
|
|||
reportRepository,
|
||||
settingsService,
|
||||
instanceSettings,
|
||||
modulesConfig,
|
||||
eventService,
|
||||
startServer: () => serverStartedListeners.forEach((listener) => listener()),
|
||||
};
|
||||
|
|
@ -120,25 +107,10 @@ describe('InstanceReportingScheduler', () => {
|
|||
});
|
||||
|
||||
describe('init', () => {
|
||||
test('fails when the insights module is disabled', async () => {
|
||||
const { scheduler } = makeHarness({ disabledModules: ['insights'] });
|
||||
|
||||
await expect(scheduler.init()).rejects.toThrow(UserError);
|
||||
});
|
||||
|
||||
test('warns and stays idle when no base URL is configured', async () => {
|
||||
const { scheduler, reportingService } = makeHarness({ baseUrl: '' });
|
||||
|
||||
await scheduler.init();
|
||||
await vi.advanceTimersByTimeAsync(Time.days.toMilliseconds);
|
||||
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not start a timer on a follower', async () => {
|
||||
const { scheduler, reportingService } = makeHarness({ isLeader: false });
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await vi.advanceTimersByTimeAsync(Time.days.toMilliseconds);
|
||||
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
|
@ -147,7 +119,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
test('fires at the configured time on the leader', async () => {
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -160,7 +132,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
test('fires once a day thereafter', async () => {
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await vi.advanceTimersByTimeAsync(3 * Time.days.toMilliseconds);
|
||||
|
||||
expect(reportingService.sendReport).toHaveBeenCalledTimes(3);
|
||||
|
|
@ -172,7 +144,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
vi.setSystemTime(new Date(AFTER_SLOT));
|
||||
const { scheduler, reportingService, startServer } = makeHarness({ autoStart: false });
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -188,7 +160,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
vi.setSystemTime(new Date(AFTER_SLOT));
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
|
||||
expect(reportingService.sendReport).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -199,7 +171,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
const { scheduler, reportingService, reportRepository } = makeHarness();
|
||||
reportRepository.hasSettledToday.mockResolvedValue(true);
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
|
@ -209,7 +181,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
vi.setSystemTime(new Date(AFTER_SLOT));
|
||||
const { scheduler, reportingService, reportRepository } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
// The catch-up delivered it, so the rest of the day has nothing to do.
|
||||
reportRepository.hasSettledToday.mockResolvedValue(true);
|
||||
|
|
@ -224,7 +196,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
vi.setSystemTime(new Date(AFTER_SLOT));
|
||||
const { scheduler, reportingService, instanceSettings } = makeHarness({ isLeader: false });
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -246,7 +218,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
vi.setSystemTime(new Date(AFTER_SLOT));
|
||||
const { scheduler, reportingService, startServer } = makeHarness({ autoStart: false });
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
scheduler.stop();
|
||||
startServer();
|
||||
await settle();
|
||||
|
|
@ -257,7 +229,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
test('stops reporting when this main steps down', async () => {
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
scheduler.stop();
|
||||
await vi.advanceTimersByTimeAsync(2 * Time.days.toMilliseconds);
|
||||
|
|
@ -268,7 +240,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
test('shutdown stops the timer and blocks a later takeover', async () => {
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
scheduler.shutdown();
|
||||
scheduler.start();
|
||||
|
|
@ -284,7 +256,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
const { scheduler, reportingService } = makeHarness();
|
||||
reportingService.sendReport.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(reportingService.sendReport).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
|
@ -303,7 +275,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
async () => reportingService.sendReport.mock.calls.length >= 3,
|
||||
);
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await vi.advanceTimersByTimeAsync(Time.hours.toMilliseconds);
|
||||
|
||||
expect(reportingService.sendReport).toHaveBeenCalledTimes(3);
|
||||
|
|
@ -316,7 +288,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
const { scheduler, reportingService } = makeHarness();
|
||||
reportingService.msUntilRetryAllowed.mockResolvedValueOnce(2 * Time.minutes.toMilliseconds);
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(reportingService.sendReport).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -329,7 +301,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
const { scheduler, settingsService } = makeHarness();
|
||||
settingsService.getReportTime.mockRejectedValueOnce(new Error('DB down'));
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
expect(settingsService.getReportTime).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
|
@ -343,7 +315,7 @@ describe('InstanceReportingScheduler', () => {
|
|||
test('a fire before the slot reports nothing and re-arms', async () => {
|
||||
const { scheduler, reportingService } = makeHarness();
|
||||
|
||||
await scheduler.init();
|
||||
scheduler.init();
|
||||
await settle();
|
||||
// Clock jumps backwards an hour, well before the slot.
|
||||
vi.setSystemTime(new Date('2026-03-26T06:41:00.000Z'));
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import type { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
|
||||
import { InstanceReportingController } from '../instance-reporting.controller';
|
||||
|
||||
const LAST_DELIVERY = new Date('2026-03-25T07:42:13.000Z');
|
||||
|
||||
describe('InstanceReportingController', () => {
|
||||
describe('getStatus()', () => {
|
||||
it('reports when the receiver last accepted a report', async () => {
|
||||
const reportRepository = mock<InstanceMonitoringReportRepository>();
|
||||
reportRepository.findLastDeliveryTime.mockResolvedValue(LAST_DELIVERY);
|
||||
|
||||
const status = await new InstanceReportingController(reportRepository).getStatus();
|
||||
|
||||
expect(status).toEqual({ lastSuccessfulReport: LAST_DELIVERY.toISOString() });
|
||||
});
|
||||
|
||||
it('reports a null last delivery until the receiver accepts a report', async () => {
|
||||
const reportRepository = mock<InstanceMonitoringReportRepository>();
|
||||
reportRepository.findLastDeliveryTime.mockResolvedValue(null);
|
||||
|
||||
const status = await new InstanceReportingController(reportRepository).getStatus();
|
||||
|
||||
expect(status).toEqual({ lastSuccessfulReport: null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import type { FrontendModuleSettings } from '@n8n/api-types';
|
||||
import type { ModuleName } from '@n8n/backend-common';
|
||||
import { Logger, ModulesConfig } from '@n8n/backend-common';
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import type { Controller } from '@n8n/decorators';
|
||||
import { ControllerRegistryMetadata, ModuleMetadata } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { InstanceMonitoringReportRepository } from '../database/repositories/instance-monitoring-report.repository';
|
||||
import { InstanceReportingScheduler } from '../instance-reporting-scheduler.service';
|
||||
import { InstanceReportingSettingsService } from '../instance-reporting-settings.service';
|
||||
import { InstanceReportingConfig } from '../instance-reporting.config';
|
||||
import { InstanceReportingModule } from '../instance-reporting.module';
|
||||
|
||||
// Compile-time pin: the settings this module returns are read by the frontend
|
||||
// under this exact key, so the decorator name must stay a member of
|
||||
// `FrontendModuleSettings`.
|
||||
const MODULE_NAME = 'instance-reporting' satisfies keyof FrontendModuleSettings;
|
||||
|
||||
const REPORT_TIME = '07:42';
|
||||
const LAST_DELIVERY = new Date('2026-03-25T07:42:13.000Z');
|
||||
|
||||
function setUpContainer({
|
||||
baseUrl = 'https://example.com',
|
||||
disabledModules = [] as ModuleName[],
|
||||
lastDelivery = LAST_DELIVERY as Date | null,
|
||||
} = {}) {
|
||||
const config = new InstanceReportingConfig();
|
||||
config.instanceReportingBaseUrl = baseUrl;
|
||||
Container.set(InstanceReportingConfig, config);
|
||||
|
||||
Container.set(ModulesConfig, mock<ModulesConfig>({ disabledModules }));
|
||||
Container.set(Logger, mockLogger());
|
||||
|
||||
const settingsService = mock<InstanceReportingSettingsService>();
|
||||
settingsService.getReportTime.mockResolvedValue(REPORT_TIME);
|
||||
Container.set(InstanceReportingSettingsService, settingsService);
|
||||
|
||||
const reportRepository = mock<InstanceMonitoringReportRepository>();
|
||||
reportRepository.findLastDeliveryTime.mockResolvedValue(lastDelivery);
|
||||
Container.set(InstanceMonitoringReportRepository, reportRepository);
|
||||
|
||||
const scheduler = mock<InstanceReportingScheduler>();
|
||||
Container.set(InstanceReportingScheduler, scheduler);
|
||||
|
||||
return { settingsService, reportRepository, scheduler };
|
||||
}
|
||||
|
||||
describe('InstanceReportingModule', () => {
|
||||
describe('settings()', () => {
|
||||
it('reports the time the instance is due to report at', async () => {
|
||||
setUpContainer();
|
||||
|
||||
const settings = await new InstanceReportingModule().settings();
|
||||
|
||||
expect(settings).toEqual({ enabled: true, reportTime: REPORT_TIME });
|
||||
});
|
||||
|
||||
it('reports as disabled, claiming no time and reading nothing, without a receiver', async () => {
|
||||
const { settingsService, reportRepository } = setUpContainer({ baseUrl: '' });
|
||||
|
||||
const settings = await new InstanceReportingModule().settings();
|
||||
|
||||
expect(settings).toEqual({ enabled: false });
|
||||
expect(settingsService.getReportTime).not.toHaveBeenCalled();
|
||||
expect(reportRepository.findLastDeliveryTime).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('starts the scheduler', async () => {
|
||||
const { scheduler } = setUpContainer();
|
||||
|
||||
await new InstanceReportingModule().init();
|
||||
|
||||
expect(scheduler.init).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails when the insights module is disabled', async () => {
|
||||
setUpContainer({ disabledModules: ['insights'] });
|
||||
|
||||
await expect(new InstanceReportingModule().init()).rejects.toThrow(UserError);
|
||||
});
|
||||
|
||||
it('leaves the scheduler alone when no receiver is configured', async () => {
|
||||
const { scheduler } = setUpContainer({ baseUrl: '' });
|
||||
|
||||
await new InstanceReportingModule().init();
|
||||
|
||||
expect(scheduler.init).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// The route belongs to the loaded module, not to the receiver, so a client
|
||||
// that asks anyway gets an answer instead of a 404.
|
||||
it('registers the status route even when no receiver is configured', async () => {
|
||||
setUpContainer({ baseUrl: '' });
|
||||
|
||||
await new InstanceReportingModule().init();
|
||||
|
||||
const { InstanceReportingController } = await import('../instance-reporting.controller.js');
|
||||
const metadata = Container.get(ControllerRegistryMetadata).getControllerMetadata(
|
||||
InstanceReportingController as Controller,
|
||||
);
|
||||
|
||||
expect(metadata.basePath).toBe('/instance-reporting');
|
||||
expect(metadata.routes.get('getStatus')?.path).toBe('/status');
|
||||
});
|
||||
});
|
||||
|
||||
it('is registered under the module-settings key the frontend reads', () => {
|
||||
expect(Container.get(ModuleMetadata).get(MODULE_NAME)?.class).toBe(InstanceReportingModule);
|
||||
});
|
||||
});
|
||||
|
|
@ -166,6 +166,43 @@ describe('InstanceMonitoringReportRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('findLastDeliveryTime', () => {
|
||||
test('is null when nothing was ever delivered', async () => {
|
||||
await repository.createPending(DATA_POINTS);
|
||||
|
||||
await expect(repository.findLastDeliveryTime()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('returns the latest delivery time across delivered reports', async () => {
|
||||
const earlier = await repository.createPending(DATA_POINTS);
|
||||
await repository.markDelivered(earlier.id, new Date('2026-03-24T07:42:00.000Z'));
|
||||
|
||||
const latest = await repository.createPending(DATA_POINTS);
|
||||
const latestDeliveredAt = new Date('2026-03-25T07:42:13.000Z');
|
||||
await repository.markDelivered(latest.id, latestDeliveredAt);
|
||||
|
||||
const lastDelivery = await repository.findLastDeliveryTime();
|
||||
|
||||
expect(lastDelivery?.toISOString()).toBe(latestDeliveredAt.toISOString());
|
||||
});
|
||||
|
||||
test('ignores reports that never reached the receiver', async () => {
|
||||
const delivered = await repository.createPending(DATA_POINTS);
|
||||
const deliveredAt = new Date('2026-03-24T07:42:00.000Z');
|
||||
await repository.markDelivered(delivered.id, deliveredAt);
|
||||
|
||||
const failed = await repository.createPending(DATA_POINTS);
|
||||
await repository.recordFailure(failed.id, 'Network error', new Date());
|
||||
const skipped = await repository.createPending(DATA_POINTS);
|
||||
await repository.markSkipped(skipped.id);
|
||||
await repository.createPending(DATA_POINTS);
|
||||
|
||||
const lastDelivery = await repository.findLastDeliveryTime();
|
||||
|
||||
expect(lastDelivery?.toISOString()).toBe(deliveredAt.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('markDelivered', () => {
|
||||
test('stamps the delivery time, counts the attempt and clears any earlier error', async () => {
|
||||
const { id } = await repository.createPending(DATA_POINTS);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Service } from '@n8n/di';
|
||||
import { DataSource, MoreThanOrEqual, Not, Repository } from '@n8n/typeorm';
|
||||
import { DataSource, IsNull, MoreThanOrEqual, Not, Repository } from '@n8n/typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { InstanceReportDataPoint } from '../entities/instance-monitoring-report';
|
||||
|
|
@ -85,6 +85,19 @@ export class InstanceMonitoringReportRepository extends Repository<InstanceMonit
|
|||
return days.length ? days.reduce((a, b) => (a > b ? a : b)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the receiver last accepted a report, or `null` when it never did.
|
||||
*/
|
||||
async findLastDeliveryTime(): Promise<Date | null> {
|
||||
const report = await this.findOne({
|
||||
where: { status: 'delivered', deliveredAt: Not(IsNull()) },
|
||||
order: { deliveredAt: 'DESC' },
|
||||
select: ['deliveredAt'],
|
||||
});
|
||||
|
||||
return report?.deliveredAt ?? null;
|
||||
}
|
||||
|
||||
async markDelivered(id: string, deliveredAt: Date): Promise<void> {
|
||||
await this.increment({ id }, 'attempts', 1);
|
||||
await this.update(
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
import { Logger, ModulesConfig } from '@n8n/backend-common';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Time } from '@n8n/constants';
|
||||
import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators';
|
||||
import { Service } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { strict } from 'node:assert';
|
||||
|
||||
import { EventService } from '@/events/event.service';
|
||||
|
||||
import { InstanceMonitoringReportRepository } from './database/repositories/instance-monitoring-report.repository';
|
||||
import { InstanceReportingSettingsService } from './instance-reporting-settings.service';
|
||||
import { InstanceReportingConfig } from './instance-reporting.config';
|
||||
import { InstanceReportingService, RETRY_DELAY_MS } from './instance-reporting.service';
|
||||
|
||||
import { EventService } from '@/events/event.service';
|
||||
|
||||
const MINUTES_PER_DAY = 24 * 60;
|
||||
|
||||
/**
|
||||
|
|
@ -43,12 +41,10 @@ export class InstanceReportingScheduler {
|
|||
private readonly onServerStarted = () => this.start();
|
||||
|
||||
constructor(
|
||||
private readonly config: InstanceReportingConfig,
|
||||
private readonly reportingService: InstanceReportingService,
|
||||
private readonly reportRepository: InstanceMonitoringReportRepository,
|
||||
private readonly settingsService: InstanceReportingSettingsService,
|
||||
private readonly instanceSettings: InstanceSettings,
|
||||
private readonly modulesConfig: ModulesConfig,
|
||||
private readonly eventService: EventService,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
|
|
@ -56,29 +52,16 @@ export class InstanceReportingScheduler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate what the module needs, then start reporting if this instance leads.
|
||||
* Start reporting if this instance leads.
|
||||
*
|
||||
* Runs from the module entrypoint, which the registry calls after
|
||||
* Runs from the module entrypoint, which checks that the module has what it
|
||||
* needs before it loads this class at all, and which the registry calls after
|
||||
* `initOrchestration` has settled this instance's role, so `isLeader` is
|
||||
* already meaningful here.
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
init(): void {
|
||||
strict(this.instanceSettings.instanceRole !== 'unset', 'Instance role is not set');
|
||||
|
||||
// The daily figure is read from insights, so the reporter cannot run without it.
|
||||
if (this.modulesConfig.disabledModules.includes('insights')) {
|
||||
throw new UserError(
|
||||
'The `instance-reporting` module requires the `insights` module, but it is listed in N8N_DISABLED_MODULES. Remove `insights` from N8N_DISABLED_MODULES or remove `instance-reporting` from N8N_ENABLED_MODULES.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.config.instanceReportingBaseUrl) {
|
||||
this.logger.warn(
|
||||
'Instance reporting is enabled but N8N_INSTANCE_REPORTING_BASE_URL is unset, so no reports will be sent',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer the first tick until the server has finished starting. A boot
|
||||
// catch-up report can send right away, but log streaming module was not properly initialized
|
||||
if (this.instanceSettings.isLeader) {
|
||||
|
|
@ -87,11 +70,7 @@ export class InstanceReportingScheduler {
|
|||
}
|
||||
|
||||
get isEnabled(): boolean {
|
||||
return (
|
||||
this.instanceSettings.instanceType === 'main' &&
|
||||
this.instanceSettings.isLeader &&
|
||||
this.config.instanceReportingBaseUrl !== ''
|
||||
);
|
||||
return this.instanceSettings.instanceType === 'main' && this.instanceSettings.isLeader;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { InstanceReportingStatus } from '@n8n/api-types';
|
||||
import { Get, RestController } from '@n8n/decorators';
|
||||
|
||||
import { InstanceMonitoringReportRepository } from './database/repositories/instance-monitoring-report.repository';
|
||||
|
||||
@RestController('/instance-reporting')
|
||||
export class InstanceReportingController {
|
||||
constructor(private readonly reportRepository: InstanceMonitoringReportRepository) {}
|
||||
|
||||
@Get('/status')
|
||||
async getStatus(): Promise<InstanceReportingStatus> {
|
||||
const lastDelivery = await this.reportRepository.findLastDeliveryTime();
|
||||
|
||||
return { lastSuccessfulReport: lastDelivery?.toISOString() ?? null };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { Logger, ModulesConfig } from '@n8n/backend-common';
|
||||
import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Reports this instance's billable execution numbers to a central monitoring
|
||||
* receiver, once a day.
|
||||
*
|
||||
* Opt-in: not a default module, so it runs only when an operator lists it in
|
||||
* `N8N_ENABLED_MODULES`. Main-only, and within a multi-main deployment only the
|
||||
* leader holds the timer, so a cluster reports once rather than once per main.
|
||||
*
|
||||
* The daily figure comes from the insights module, but the receiver only sees
|
||||
* data points, so that source is an implementation detail of
|
||||
* `InstanceReportingService` rather than part of the reporting contract.
|
||||
*/
|
||||
@BackendModule({ name: 'instance-reporting', instanceTypes: ['main'] })
|
||||
export class InstanceReportingModule implements ModuleInterface {
|
||||
async init() {
|
||||
// Imported before the receiver check, so the route exists whenever the
|
||||
// module is loaded. The client gates off `enabled` in the module settings.
|
||||
await import('./instance-reporting.controller.js');
|
||||
|
||||
// The daily figure is read from insights, so the reporter cannot run without it.
|
||||
if (Container.get(ModulesConfig).disabledModules.includes('insights')) {
|
||||
throw new UserError(
|
||||
'The `instance-reporting` module requires the `insights` module, but it is listed in N8N_DISABLED_MODULES. Remove `insights` from N8N_DISABLED_MODULES or remove `instance-reporting` from N8N_ENABLED_MODULES.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await this.isConfigured())) {
|
||||
Container.get(Logger)
|
||||
.scoped('instance-reporting')
|
||||
.warn(
|
||||
'Instance reporting is enabled but N8N_INSTANCE_REPORTING_BASE_URL is unset, so no reports will be sent',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { InstanceReportingScheduler } = await import(
|
||||
'./instance-reporting-scheduler.service.js'
|
||||
);
|
||||
|
||||
Container.get(InstanceReportingScheduler).init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings exposed to the frontend under `/rest/module-settings`.
|
||||
*
|
||||
* Return values:
|
||||
* { enabled: false } - module loaded but no receiver configured
|
||||
* { enabled: true, reportTime: 'HH:mm' } - module loaded and receiver configured
|
||||
*
|
||||
* Built once at startup and cached for the process lifetime.
|
||||
**/
|
||||
async settings() {
|
||||
if (!(await this.isConfigured())) return { enabled: false };
|
||||
|
||||
const { InstanceReportingSettingsService } = await import(
|
||||
'./instance-reporting-settings.service.js'
|
||||
);
|
||||
|
||||
const reportTime = await Container.get(InstanceReportingSettingsService).getReportTime();
|
||||
|
||||
return { enabled: true, reportTime };
|
||||
}
|
||||
|
||||
async entities() {
|
||||
const { InstanceMonitoringReport } = await import(
|
||||
'./database/entities/instance-monitoring-report.js'
|
||||
);
|
||||
|
||||
return [InstanceMonitoringReport];
|
||||
}
|
||||
|
||||
/** Whether a receiver is configured, i.e. whether reports are actually sent. */
|
||||
private async isConfigured(): Promise<boolean> {
|
||||
const { InstanceReportingConfig } = await import('./instance-reporting.config.js');
|
||||
|
||||
return Container.get(InstanceReportingConfig).instanceReportingBaseUrl !== '';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
/**
|
||||
* Reports this instance's billable execution numbers to a central monitoring
|
||||
* receiver, once a day.
|
||||
*
|
||||
* Opt-in: not a default module, so it runs only when an operator lists it in
|
||||
* `N8N_ENABLED_MODULES`. Main-only, and within a multi-main deployment only the
|
||||
* leader holds the timer, so a cluster reports once rather than once per main.
|
||||
*
|
||||
* The daily figure comes from the insights module, but the receiver only sees
|
||||
* data points, so that source is an implementation detail of
|
||||
* `InstanceReportingService` rather than part of the reporting contract.
|
||||
*/
|
||||
@BackendModule({ name: 'instance-reporting', instanceTypes: ['main'] })
|
||||
export class InstanceReportingModule implements ModuleInterface {
|
||||
async init() {
|
||||
const { InstanceReportingScheduler } = await import(
|
||||
'./instance-reporting-scheduler.service.js'
|
||||
);
|
||||
|
||||
await Container.get(InstanceReportingScheduler).init();
|
||||
}
|
||||
|
||||
async entities() {
|
||||
const { InstanceMonitoringReport } = await import(
|
||||
'./database/entities/instance-monitoring-report.js'
|
||||
);
|
||||
|
||||
return [InstanceMonitoringReport];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export * from './credentialResolvers';
|
|||
export * from './ctas';
|
||||
export * from './eventbus.ee';
|
||||
export * from './instance-registry';
|
||||
export * from './instance-reporting';
|
||||
export * from './instance-version-history';
|
||||
export * from './events';
|
||||
export * from './externalSecrets.ee';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import type { InstanceReportingStatus } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getInstanceReportingStatus(
|
||||
context: IRestApiContext,
|
||||
): Promise<InstanceReportingStatus> {
|
||||
return await makeRestApiRequest(context, 'GET', '/instance-reporting/status');
|
||||
}
|
||||
398
scripts/lint-parity/samples.json
Normal file
398
scripts/lint-parity/samples.json
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
{
|
||||
"packages/@n8n/agents": [
|
||||
"src/__tests__/agent-configuration.test.ts",
|
||||
"src/__tests__/integration/agent-vector-store-pinecone.test.ts",
|
||||
"src/evals/categorization.ts"
|
||||
],
|
||||
"packages/@n8n/ai-node-sdk": ["src/index.ts"],
|
||||
"packages/@n8n/ai-utilities": [
|
||||
"integration-tests/openai.test.ts",
|
||||
"src/__tests__/adapters/langchain-chat-model.test.ts",
|
||||
"src/adapters/langchain-chat-model.ts"
|
||||
],
|
||||
"packages/@n8n/ai-workflow-builder.ee": [
|
||||
"evaluations/__tests__/argument-parser.test.ts",
|
||||
"src/agents/planner.agent.ts",
|
||||
"src/test/ai-workflow-builder-agent.service.test.ts"
|
||||
],
|
||||
"packages/@n8n/api-types": [
|
||||
"src/__tests__/agent-builder-interactive.test.ts",
|
||||
"src/agent-builder-interactive.ts"
|
||||
],
|
||||
"packages/@n8n/backend-common": ["src/__tests__/cli-parser.test.ts", "src/cli-parser.ts"],
|
||||
"packages/@n8n/backend-network": [
|
||||
"src/dns/__tests__/dns-resolver.test.ts",
|
||||
"src/dns/dns-resolver.ts"
|
||||
],
|
||||
"packages/@n8n/backend-test-utils": ["src/db/projects.ts"],
|
||||
"packages/@n8n/benchmark": ["src/commands/list.ts"],
|
||||
"packages/@n8n/blob-storage": [
|
||||
"src/__tests__/azure-byte-store.test.ts",
|
||||
"src/azure-blob/azure-blob.config.ts"
|
||||
],
|
||||
"packages/@n8n/chat-hub": ["src/artifact.test.ts", "src/artifact.ts"],
|
||||
"packages/@n8n/cli": [
|
||||
"src/__tests__/client.test.ts",
|
||||
"src/base-command.ts",
|
||||
"src/client.ts",
|
||||
"src/commands/login.ts",
|
||||
"src/index.ts"
|
||||
],
|
||||
"packages/@n8n/client-oauth2": ["src/client-oauth2-token.ts", "test/client-oauth2.test.ts"],
|
||||
"packages/@n8n/codemirror-lang-sql": ["src/complete.ts", "test/complete.test.ts"],
|
||||
"packages/@n8n/codemirror-lang": [
|
||||
"src/expressions/grammar.terms.ts",
|
||||
"test/expressions/expressions.test.ts"
|
||||
],
|
||||
"packages/@n8n/computer-use": [
|
||||
"src/__mocks__/@inquirer/prompts.ts",
|
||||
"src/config-templates.test.ts"
|
||||
],
|
||||
"packages/@n8n/config": [
|
||||
"src/configs/__tests__/ai.config.test.ts",
|
||||
"src/configs/activity-log.config.ts"
|
||||
],
|
||||
"packages/@n8n/constants": ["src/api.ts"],
|
||||
"packages/@n8n/crdt": [
|
||||
"src/__tests__/helpers.ts",
|
||||
"src/awareness/awareness.test.ts",
|
||||
"src/awareness/index.ts"
|
||||
],
|
||||
"packages/@n8n/db": [
|
||||
"src/connection/__tests__/backoff.test.ts",
|
||||
"src/connection/backoff.ts",
|
||||
"src/migrations/__tests__/migration-helpers.ts"
|
||||
],
|
||||
"packages/@n8n/decorators": [
|
||||
"src/__tests__/memoized.test.ts",
|
||||
"src/auth-handler/auth-handler-metadata.ts"
|
||||
],
|
||||
"packages/@n8n/di": ["src/__tests__/circular-depedency.test.ts", "src/di.ts"],
|
||||
"packages/@n8n/engine": [
|
||||
"src/admittance/__tests__/allow-all-admittance.test.ts",
|
||||
"src/admittance/admittance-rejected.error.ts"
|
||||
],
|
||||
"packages/@n8n/errors": ["src/application.error.ts"],
|
||||
"packages/@n8n/eslint-plugin-community-nodes": [
|
||||
"src/plugin.ts",
|
||||
"src/rules/ai-node-package-json.test.ts"
|
||||
],
|
||||
"packages/@n8n/extension-sdk": ["src/backend/define.ts"],
|
||||
"packages/@n8n/imap": ["src/connection-options.test.ts", "src/connection-options.ts"],
|
||||
"packages/@n8n/instance-ai": [
|
||||
"evaluations/__tests__/agent-report-rendering.test.ts",
|
||||
"evaluations/computer-use/report-html.ts",
|
||||
"src/__tests__/binary-checks.test.ts",
|
||||
"src/agent/aia-model-defaults.ts",
|
||||
"src/tools/__tests__/activity.tool.test.ts"
|
||||
],
|
||||
"packages/@n8n/json-schema-to-zod": ["src/index.ts", "test/json-schema-to-zod.test.ts"],
|
||||
"packages/@n8n/local-gateway": ["src/main/connect-origin.test.ts", "src/main/connect-origin.ts"],
|
||||
"packages/@n8n/mcp-apps": [
|
||||
"src/apps-manifest.ts",
|
||||
"src/apps/workflow-preview/App.vue",
|
||||
"src/apps/workflow-preview/composables/use-workflow-preview.test.ts"
|
||||
],
|
||||
"packages/@n8n/mcp-browser-extension": [
|
||||
"src/approvedHosts.test.ts",
|
||||
"src/approvedHosts.ts",
|
||||
"src/ui/App.vue"
|
||||
],
|
||||
"packages/@n8n/mcp-browser": [
|
||||
"src/__tests__/cdp-relay-attach.test.ts",
|
||||
"src/adapters/agent-browser.ts"
|
||||
],
|
||||
"packages/@n8n/node-cli": [
|
||||
"src/commands/build.test.ts",
|
||||
"src/commands/build.ts",
|
||||
"src/configs/eslint.ts",
|
||||
"src/modules.d.ts",
|
||||
"src/test-utils/command-tester.ts"
|
||||
],
|
||||
"packages/@n8n/node-engine-compatibility": [
|
||||
"src/__tests__/acceptance-fixtures.ts",
|
||||
"src/__tests__/engine-step-data-loader.test.ts",
|
||||
"src/constants.ts"
|
||||
],
|
||||
"packages/@n8n/nodes-langchain": [
|
||||
"credentials/AlibabaCloudApi.credentials.ts",
|
||||
"credentials/test/AnthropicApi.credentials.test.ts",
|
||||
"nodes/Guardrails/Guardrails.node.ts",
|
||||
"nodes/Guardrails/helpers/__tests__/model.test.ts",
|
||||
"nodes/mcp/McpClient/__test__/McpClient.node.test.ts"
|
||||
],
|
||||
"packages/@n8n/permissions": ["src/__tests__/schemas.test.ts", "src/constants.ee.ts"],
|
||||
"packages/@n8n/scheduler": ["src/__tests__/dependency-purity.test.ts", "src/core/clock-skew.ts"],
|
||||
"packages/@n8n/syslog-client": ["src/client.ts", "test/client.test.ts", "vite.config.ts"],
|
||||
"packages/@n8n/task-runner": [
|
||||
"src/__tests__/node-types.test.ts",
|
||||
"src/config/base-runner-config.ts"
|
||||
],
|
||||
"packages/@n8n/telemetry": ["src/__tests__/catalog.test.ts", "src/catalog.ts"],
|
||||
"packages/@n8n/tournament": ["src/Constants.ts", "test/ASTHooks.test.ts"],
|
||||
"packages/@n8n/utils": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/assert.ts",
|
||||
"src/client-assertion.test.ts"
|
||||
],
|
||||
"packages/@n8n/workflow-sdk": [
|
||||
"src/__tests__/fixtures-download.ts",
|
||||
"src/ast-interpreter/errors.ts",
|
||||
"src/ast-interpreter/interpreter.test.ts",
|
||||
"src/prompts/best-practices/guides/chatbot.ts",
|
||||
"src/prompts/node-guidance/parameter-guides/embedding-nodes.ts"
|
||||
],
|
||||
"packages/cli": [
|
||||
"src/__tests__/abstract-server.test.ts",
|
||||
"src/abstract-server.ts",
|
||||
"src/active-workflow-manager.ts",
|
||||
"src/binary-data/database.manager.ts",
|
||||
"src/commands/db/revert.ts",
|
||||
"src/commands/import/credentials.ts",
|
||||
"src/commands/ldap/reset.ts",
|
||||
"src/controllers/project.controller.ts",
|
||||
"src/credentials-helper.ts",
|
||||
"src/credentials/credential-connection-status-provider.interface.ts",
|
||||
"src/credentials/credential-connection-status-proxy.ts",
|
||||
"src/credentials/credential-dependency.service.ts",
|
||||
"src/credentials/credentials-finder.service.ts",
|
||||
"src/credentials/credentials.controller.ts",
|
||||
"src/credentials/credentials.service.ee.ts",
|
||||
"src/credentials/credentials.service.ts",
|
||||
"src/databases/repositories/__tests__/credentials.repository.test.ts",
|
||||
"src/evaluation.ee/evaluation-collection.service.ts",
|
||||
"src/evaluation.ee/test-runner/test-runner.service.ee.ts",
|
||||
"src/eventbus/message-event-bus/message-event-bus.ts",
|
||||
"src/events/relays/telemetry.event-relay.ts",
|
||||
"src/executions/execution-data/db-store.ts",
|
||||
"src/executions/execution-persistence.ts",
|
||||
"src/executions/execution-recovery.service.ts",
|
||||
"src/executions/execution.service.ts",
|
||||
"src/instance-settings-loader/loaders/log-streaming.instance-settings-loader.ts",
|
||||
"src/license.ts",
|
||||
"src/metrics/prometheus/instance-role-metrics.service.ts",
|
||||
"src/modules/agent-evals/agent-evals.module.ts",
|
||||
"src/modules/agents/agent-knowledge.service.ts",
|
||||
"src/modules/agents/agent-publish.service.ts",
|
||||
"src/modules/agents/agent-task.service.ts",
|
||||
"src/modules/agents/agents.service.ts",
|
||||
"src/modules/agents/builder/agents-builder.service.ts",
|
||||
"src/modules/agents/entities/agent-background-job.entity.ts",
|
||||
"src/modules/agents/instance-ai-builder-delegate.adapter.ts",
|
||||
"src/modules/agents/integrations/agent-channel-reconciler.service.ts",
|
||||
"src/modules/agents/integrations/leader-channel-relay.service.ts",
|
||||
"src/modules/agents/integrations/n8n-memory.ts",
|
||||
"src/modules/agents/integrations/platforms/discord-integration.ts",
|
||||
"src/modules/agents/repositories/agent-background-job.repository.ts",
|
||||
"src/modules/agents/tools/workflow-tool-workflow-resolver.ts",
|
||||
"src/modules/breaking-changes/breaking-changes.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-agent.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-credentials.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-title.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-tool.service.ts",
|
||||
"src/modules/chat-hub/chat-hub-workflow.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.attachment.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.models.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.service.ts",
|
||||
"src/modules/chat-hub/chat-hub.settings.service.ts",
|
||||
"src/modules/data-table/data-table-ddl.service.ts",
|
||||
"src/modules/data-table/data-table.service.ts",
|
||||
"src/modules/data-table/utils/sql-utils.ts",
|
||||
"src/modules/dynamic-credentials.ee/database/entities/credential-resolver.ts",
|
||||
"src/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-connection-status.service.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-resolver-workflow.service.ts",
|
||||
"src/modules/dynamic-credentials.ee/services/credential-resolver.service.ts",
|
||||
"src/modules/external-secrets.ee/secrets-providers-connections.service.ee.ts",
|
||||
"src/modules/favorites/favorites.service.ts",
|
||||
"src/modules/insights/insights-collection.service.ts",
|
||||
"src/modules/instance-ai/__tests__/agent-preview-handoff.test.ts",
|
||||
"src/modules/instance-ai/instance-ai.adapter.service.ts",
|
||||
"src/modules/instance-ai/mcp/instance-ai-mcp-registry.service.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-agent-checkpoint-store.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-agent-memory.ts",
|
||||
"src/modules/instance-ai/storage/typeorm-observation-log-store.ts",
|
||||
"src/modules/instance-ai/suspended-thread-persistence.service.ts",
|
||||
"src/modules/instance-reporting.ee/instance-reporting-scheduler.service.ts",
|
||||
"src/modules/log-streaming.ee/log-streaming-destination.service.ts",
|
||||
"src/modules/mcp/mcp-api-key.service.ts",
|
||||
"src/modules/mcp/mcp.settings.service.ts",
|
||||
"src/modules/oauth-jwe/oauth-jwe-key.service.ts",
|
||||
"src/modules/provisioning.ee/provisioning.service.ee.ts",
|
||||
"src/modules/provisioning.ee/role-mapping-rule.service.ee.ts",
|
||||
"src/modules/provisioning.ee/role-resolver.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-context.factory.ts",
|
||||
"src/modules/source-control.ee/source-control-export.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-import.service.ee.ts",
|
||||
"src/modules/source-control.ee/source-control-scoped.service.ts",
|
||||
"src/modules/source-control.ee/source-control-status.service.ee.ts",
|
||||
"src/modules/token-exchange/services/trusted-key.service.ts",
|
||||
"src/modules/workflow-index/workflow-dependency-query.service.ts",
|
||||
"src/permissions.ee/check-access.ts",
|
||||
"src/policy/policy-enforcement.service.ts",
|
||||
"src/public-api/v1/controllers/__tests__/workflows.public.controller.test.ts",
|
||||
"src/public-api/v1/handlers/audit/audit.handler.ts",
|
||||
"src/public-api/v1/handlers/community-packages/community-packages.handler.ts",
|
||||
"src/public-api/v1/handlers/credentials/credentials.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.columns.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.handler.ts",
|
||||
"src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts",
|
||||
"src/public-api/v1/handlers/discover/discover.handler.ts",
|
||||
"src/public-api/v1/handlers/evaluations/evaluations.handler.ts",
|
||||
"src/public-api/v1/handlers/folders/folders.handler.ts",
|
||||
"src/public-api/v1/handlers/insights/insights.handler.ts",
|
||||
"src/public-api/v1/handlers/ldap/ldap.handler.ts",
|
||||
"src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts",
|
||||
"src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts",
|
||||
"src/public-api/v1/handlers/otel/otel.handler.ts",
|
||||
"src/public-api/v1/handlers/projects/projects.handler.ts",
|
||||
"src/public-api/v1/handlers/security-policy/security-policy.handler.ts",
|
||||
"src/public-api/v1/handlers/sso-oidc/sso-oidc.handler.ts",
|
||||
"src/public-api/v1/handlers/sso-saml/sso-saml.handler.ts",
|
||||
"src/public-api/v1/handlers/tags/tags.handler.ts",
|
||||
"src/public-api/v1/handlers/users/users.handler.ee.ts",
|
||||
"src/public-api/v1/handlers/variables/variables.handler.ts",
|
||||
"src/public-api/v1/handlers/workflows/workflows.handler.ts",
|
||||
"src/scaling/scaling.service.ts",
|
||||
"src/scheduling/durable-job-provisioner.ts",
|
||||
"src/scheduling/durable-scheduler.ts",
|
||||
"src/scheduling/poll-trigger-node/poll-trigger-job-registrar.ts",
|
||||
"src/scheduling/schedule-trigger-node/schedule-trigger-job-registrar.ts",
|
||||
"src/scheduling/system-tasks/system-task-runner.ts",
|
||||
"src/security-audit/risk-reporters/credentials-risk-reporter.ts",
|
||||
"src/security-audit/security-audit.repository.ts",
|
||||
"src/services/export.service.ts",
|
||||
"src/services/folder-finder.service.ts",
|
||||
"src/services/folder.service.ts",
|
||||
"src/services/hooks.service.ts",
|
||||
"src/services/import.service.ts",
|
||||
"src/services/ownership-transfer/ownership-transfer-handler.registry.ts",
|
||||
"src/services/ownership.service.ts",
|
||||
"src/services/project.service.ee.ts",
|
||||
"src/services/pruning/executions-pruning.service.ts",
|
||||
"src/services/pruning/workflow-history-compaction.service.ts",
|
||||
"src/services/public-api-key.service.ts",
|
||||
"src/services/role-cache.service.ts",
|
||||
"src/services/role.service.ts",
|
||||
"src/services/tag.service.ts",
|
||||
"src/services/user.service.ts",
|
||||
"src/services/workflow-statistics-rollup.service.ts",
|
||||
"src/wait-tracker.ts",
|
||||
"src/workflows/publication/workflow-publication-outbox-consumer.ts",
|
||||
"src/workflows/publication/workflow-publication-reconciler.service.ts",
|
||||
"src/workflows/workflow-creation.service.ts",
|
||||
"src/workflows/workflow-finder.service.ts",
|
||||
"src/workflows/workflow-history/workflow-history.service.ts",
|
||||
"src/workflows/workflow-sharing.service.ts",
|
||||
"src/workflows/workflow-validation.service.ts",
|
||||
"src/workflows/workflow.service.ee.ts",
|
||||
"src/workflows/workflow.service.ts",
|
||||
"src/workflows/workflows.controller.ts",
|
||||
"test/extend-expect.ts"
|
||||
],
|
||||
"packages/core": [
|
||||
"src/__tests__/credentials.test.ts",
|
||||
"src/binary-data/binary-data.config.ts",
|
||||
"test/helpers/constants.ts"
|
||||
],
|
||||
"packages/extensions/insights": [
|
||||
"src/backend/index.ts",
|
||||
"src/frontend/InsightsDashboard.vue",
|
||||
"src/frontend/index.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/chat": [
|
||||
"src/App.vue",
|
||||
"src/__stories__/App.stories.ts",
|
||||
"src/__tests__/Chat.spec.ts",
|
||||
"src/__tests__/plugins/chat.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/composables": [
|
||||
"src/__tests__/packageBoundary.test.ts",
|
||||
"src/injectionKeys.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/design-system": [
|
||||
"src/__tests__/render.ts",
|
||||
"src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts",
|
||||
"src/components/AskAssistantAvatar/AssistantAvatar.stories.ts",
|
||||
"src/components/AskAssistantAvatar/AssistantAvatar.vue",
|
||||
"src/components/N8nFormInput/validators.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/eslint-plugin-design-system": [
|
||||
"src/plugin.ts",
|
||||
"src/rules/require-teleported-tooltip-in-dropdown.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/frontend-constants": ["src/urls.ts", "src/views.test.ts", "src/views.ts"],
|
||||
"packages/frontend/@n8n/frontend-module-sdk": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts",
|
||||
"src/registries/commandRegistry.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/frontend-test-utils": ["src/__tests__/render.test.ts", "src/defaults.ts"],
|
||||
"packages/frontend/@n8n/frontend-utils": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/constants/sanitization.ts",
|
||||
"src/htmlUtils.test.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/i18n": ["src/__tests__/setup.ts", "src/index.test.ts", "src/index.ts"],
|
||||
"packages/frontend/@n8n/rest-api-client": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/api/__tests__/api-keys.test.ts",
|
||||
"src/api/ai-usage.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/stores": [
|
||||
"src/__tests__/metaTagConfig.test.ts",
|
||||
"src/cloudPlan.store.ts"
|
||||
],
|
||||
"packages/frontend/@n8n/storybook": [".storybook/ThemedDocsContainer.ts"],
|
||||
"packages/frontend/editor-ui": [
|
||||
"src/Interface.ts",
|
||||
"src/__tests__/data/users.ts",
|
||||
"src/app/App.vue",
|
||||
"src/app/__tests__/modal-definition.test.ts",
|
||||
"src/app/constants/modals.ts",
|
||||
"src/app/stores/defaults/modals.ts",
|
||||
"src/app/stores/workflowDocument/types.ts",
|
||||
"src/app/stores/workflows.store.ts",
|
||||
"src/features/agents/__tests__/AgentAdvancedPanel.test.ts",
|
||||
"src/features/agents/channels/AgentChannelStandardEditView.vue",
|
||||
"src/features/ai/chatHub/__test__/data.ts",
|
||||
"src/features/settings/environments.ee/completions/variables.completions.test.ts",
|
||||
"src/features/shared/editors/components/CodeNodeEditor/CodeNodeEditor.vue",
|
||||
"src/features/shared/editors/plugins/codemirror/completions/__tests__/mock.ts",
|
||||
"src/features/shared/toolsConnection/ToolsConnectionModal.stories.ts"
|
||||
],
|
||||
"packages/modules/insights/frontend": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/chartjs.utils.ts",
|
||||
"src/components/InsightsDashboard.test.ts",
|
||||
"src/components/InsightsDashboard.vue"
|
||||
],
|
||||
"packages/modules/instance-registry/frontend": [
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts",
|
||||
"src/instance-registry.store.test.ts"
|
||||
],
|
||||
"packages/modules/otel/frontend": [
|
||||
"src/OtelSettingsRow.vue",
|
||||
"src/SettingsOpenTelemetryView.test.ts",
|
||||
"src/__tests__/setup.ts",
|
||||
"src/index.ts"
|
||||
],
|
||||
"packages/node-dev": ["src/Build.ts"],
|
||||
"packages/nodes-base": [
|
||||
"credentials/ActionNetworkApi.credentials.ts",
|
||||
"credentials/common/aws/s3-path-signing.test.ts",
|
||||
"credentials/test/AtlassianOAuth2Api.credentials.test.ts",
|
||||
"nodes/ActionNetwork/ActionNetwork.node.ts",
|
||||
"nodes/Aws/__tests__/AwsLambda.node.test.ts"
|
||||
],
|
||||
"packages/testing/code-health": ["src/cli.ts", "src/cli/arg-parser.test.ts"],
|
||||
"packages/testing/janitor": ["src/cli.test.ts", "src/cli.ts"],
|
||||
"packages/testing/playwright": ["coverage-options.test.ts"],
|
||||
"packages/testing/rules-engine": ["src/ast/ast-rule.ts", "src/ast/imports.test.ts"],
|
||||
"packages/testing/test-impact": ["src/changes.test.ts", "src/changes.ts"],
|
||||
"packages/workflow": [
|
||||
"src/augment-object.ts",
|
||||
"test/ExpressionExtensions/array-extensions.test.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user