From 7ceb84f06c193b536ca476841800a8ed44cdca32 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 12 Oct 2026 13:38:00 +0000 Subject: [PATCH] fix(editor): Fix the last updated label in the migration report (no-changelog) (#40889) Co-authored-by: Claude Opus 5.5 (1M context) --- .../src/schemas/breaking-changes.schema.ts | 3 ++- .../telemetry/src/events/migration-report.ts | 3 ++- .../__tests__/telemetry-event-relay.test.ts | 24 +++++++++++++++++ .../events/relays/telemetry.event-relay.ts | 4 ++- .../migration-finding-query.service.test.ts | 12 +++------ .../query/migration-finding-query.service.ts | 3 +-- .../frontend/@n8n/i18n/src/locales/en.json | 3 ++- .../migrationReport/MigrationRules.test.ts | 26 ++++++++++++++----- .../migrationReport/MigrationRules.vue | 14 ++++++++-- 9 files changed, 69 insertions(+), 23 deletions(-) diff --git a/packages/@n8n/api-types/src/schemas/breaking-changes.schema.ts b/packages/@n8n/api-types/src/schemas/breaking-changes.schema.ts index 1743e278a76..a39ee63d73e 100644 --- a/packages/@n8n/api-types/src/schemas/breaking-changes.schema.ts +++ b/packages/@n8n/api-types/src/schemas/breaking-changes.schema.ts @@ -156,7 +156,8 @@ const breakingChangeReportDataSchema = { const breakingChangeReportSchema = z.object(breakingChangeReportDataSchema).strict(); const breakingChangeLightReportDataSchema = { - generatedAt: z.date(), + // Absent until the first sync of the target version completes. + generatedAt: z.date().optional(), targetVersion: z.string(), currentVersion: z.string(), instanceResults: z.array(instanceRuleResultsSchema), diff --git a/packages/@n8n/telemetry/src/events/migration-report.ts b/packages/@n8n/telemetry/src/events/migration-report.ts index 4ee6c2e35f3..0ff382092a0 100644 --- a/packages/@n8n/telemetry/src/events/migration-report.ts +++ b/packages/@n8n/telemetry/src/events/migration-report.ts @@ -33,8 +33,9 @@ export const MIGRATION_REPORT_TELEMETRY = defineTelemetryEvents({ .describe('One entry per workflow rule with at least one open finding'), synced_at: z .string() + .optional() .describe( - 'ISO time the finding table was last filled, which the overview shows as the report date', + 'ISO time the finding table was last filled, which the overview shows as the report date. Absent before the first sync', ), }), }, diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts index 7850b4ac2a7..4068d260ecc 100644 --- a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -4672,6 +4672,30 @@ describe('TelemetryEventRelay', () => { }, ); }); + + it('omits synced_at when the report has no sync time', () => { + eventService.emit('migration-report-viewed', { + user: { id: 'user-1' }, + targetVersion: 'v3', + refreshed: false, + report: { + report: { + targetVersion: 'v3', + currentVersion: '2.0.0', + instanceResults: [], + workflowResults: [], + }, + totalWorkflows: 0, + totalAffectedWorkflows: 0, + shouldCache: false, + }, + }); + + expect(telemetry.track).toHaveBeenCalledWith( + TELEMETRY_EVENT.MIGRATION_REPORT.USER_VIEWED_MIGRATION_REPORT, + expect.objectContaining({ synced_at: undefined }), + ); + }); }); describe('workflow history compaction events', () => { diff --git a/packages/cli/src/events/relays/telemetry.event-relay.ts b/packages/cli/src/events/relays/telemetry.event-relay.ts index 182c6900d92..4bca4c1d3b5 100644 --- a/packages/cli/src/events/relays/telemetry.event-relay.ts +++ b/packages/cli/src/events/relays/telemetry.event-relay.ts @@ -2408,7 +2408,9 @@ export class TelemetryEventRelay extends EventRelay { impact: rule.ruleImpact, affected_workflows: rule.nbAffectedWorkflows, })), - synced_at: new Date(report.report.generatedAt).toISOString(), + synced_at: report.report.generatedAt + ? new Date(report.report.generatedAt).toISOString() + : undefined, }); } diff --git a/packages/cli/src/modules/breaking-changes/query/__tests__/migration-finding-query.service.test.ts b/packages/cli/src/modules/breaking-changes/query/__tests__/migration-finding-query.service.test.ts index 2708f2c9991..7e8ba447f30 100644 --- a/packages/cli/src/modules/breaking-changes/query/__tests__/migration-finding-query.service.test.ts +++ b/packages/cli/src/modules/breaking-changes/query/__tests__/migration-finding-query.service.test.ts @@ -325,16 +325,10 @@ describe('MigrationFindingQueryService', () => { expect(workflowRepository.count).not.toHaveBeenCalled(); }); - it('falls back to the current time as generatedAt when no sync ran yet', async () => { - const now = new Date('2026-06-01T12:00:00.000Z'); - vi.useFakeTimers({ now }); - try { - const result = await service.getLightReport(TARGET_VERSION, INSTANCE); + it('reports no generatedAt when no sync ran yet', async () => { + const result = await service.getLightReport(TARGET_VERSION, INSTANCE); - expect(result.report.generatedAt).toEqual(now); - } finally { - vi.useRealTimers(); - } + expect(result.report.generatedAt).toBeUndefined(); }); }); diff --git a/packages/cli/src/modules/breaking-changes/query/migration-finding-query.service.ts b/packages/cli/src/modules/breaking-changes/query/migration-finding-query.service.ts index 3cae790a601..09cfda011ad 100644 --- a/packages/cli/src/modules/breaking-changes/query/migration-finding-query.service.ts +++ b/packages/cli/src/modules/breaking-changes/query/migration-finding-query.service.ts @@ -134,8 +134,7 @@ export class MigrationFindingQueryService { return { report: { - // Before the first sync there is nothing to date, so the read time stands in. - generatedAt: sync?.syncedAt ?? new Date(), + generatedAt: sync?.syncedAt, targetVersion, currentVersion: N8N_VERSION, instanceResults, diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 926e4d2458e..cf8f9118ddc 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -4613,7 +4613,8 @@ "settings.migrationReport.progress.label": "{compatibleCount} of {totalCount} compatible", "settings.migrationReport.refreshButton": "Refresh", "settings.migrationReport.scopeNote": "This report only includes the workflows you can edit.", - "settings.migrationReport.lastSynced": "Last synced {time}", + "settings.migrationReport.lastUpdated": "Last updated {time}", + "settings.migrationReport.neverUpdated": "Never updated", "settings.migrationReport.tabs.workflowIssues": "Workflow issues", "settings.migrationReport.tabs.instanceIssues": "Instance issues", "settings.migrationReport.impact.upgradeBlocked": "Upgrade blocked", diff --git a/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.test.ts b/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.test.ts index 3d5ea2fa835..2786d1159c4 100644 --- a/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.test.ts @@ -545,7 +545,7 @@ describe('MigrationRules', () => { }, ); - it('should show when the report was last synced, from generatedAt', async () => { + it('should show when the report was last updated, from generatedAt', async () => { // One year in the past, so the relative label is stable whatever the test date. const generatedAt = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); vi.mocked(breakingChangesApi.getReport).mockResolvedValue( @@ -563,12 +563,26 @@ describe('MigrationRules', () => { renderComponent(); await waitFor(() => { - const lastSynced = screen.getByTestId('migration-report-last-synced'); - expect(lastSynced).toHaveTextContent(/Last synced\s+1 year ago/); + const lastUpdated = screen.getByTestId('migration-report-last-updated'); + expect(lastUpdated).toHaveTextContent(/Last updated\s+1 year ago/); }); }); - it('should update the last synced time after Refresh', async () => { + it('should show "Never updated" when the report has no generatedAt', async () => { + vi.mocked(breakingChangesApi.getReport).mockResolvedValue( + createMockReport({ report: { ...mockReport.report, generatedAt: undefined } }), + ); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent( + 'Never updated', + ); + }); + }); + + it('should update the last updated time after Refresh', async () => { const oldDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); const newDate = new Date(Date.now() - 60 * 1000); vi.mocked(breakingChangesApi.getReport).mockResolvedValue( @@ -581,13 +595,13 @@ describe('MigrationRules', () => { renderComponent(); await waitFor(() => { - expect(screen.getByTestId('migration-report-last-synced')).toHaveTextContent(/1 year ago/); + expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent(/1 year ago/); }); await userEvent.click(screen.getByText('Refresh')); await waitFor(() => { - expect(screen.getByTestId('migration-report-last-synced')).toHaveTextContent( + expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent( /1 minute ago/, ); }); diff --git a/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.vue b/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.vue index 18711fa2304..d3bcab4ec06 100644 --- a/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.vue +++ b/packages/frontend/editor-ui/src/features/settings/migrationReport/MigrationRules.vue @@ -218,14 +218,24 @@ const sortedInstanceResults = computed(() => { v-if="state?.report.generatedAt" size="small" color="text-light" - data-test-id="migration-report-last-synced" + :class="$style.NoLineBreak" + data-test-id="migration-report-last-updated" > - + + + {{ i18n.baseText('settings.migrationReport.neverUpdated') }} +