feat(API): Return null deviation on insights summary if previous period has no data (#14193)

This commit is contained in:
Guillaume Jacquart
2025-03-26 17:56:56 +01:00
committed by GitHub
parent 41b1797a25
commit ffc0a596e0
9 changed files with 79 additions and 24 deletions

View File

@@ -15,27 +15,27 @@ export type InsightsSummaryUnit = z.infer<typeof insightsSummaryUnitSchema>;
export const insightsSummaryDataSchemas = { export const insightsSummaryDataSchemas = {
total: z.object({ total: z.object({
value: z.number(), value: z.number(),
deviation: z.number(), deviation: z.union([z.null(), z.number()]),
unit: z.literal('count'), unit: z.literal('count'),
}), }),
failed: z.object({ failed: z.object({
value: z.number(), value: z.number(),
deviation: z.number(), deviation: z.union([z.null(), z.number()]),
unit: z.literal('count'), unit: z.literal('count'),
}), }),
failureRate: z.object({ failureRate: z.object({
value: z.number(), value: z.number(),
deviation: z.number(), deviation: z.union([z.null(), z.number()]),
unit: z.literal('ratio'), unit: z.literal('ratio'),
}), }),
timeSaved: z.object({ timeSaved: z.object({
value: z.number(), value: z.number(),
deviation: z.number(), deviation: z.union([z.null(), z.number()]),
unit: z.literal('time'), unit: z.literal('time'),
}), }),
averageRunTime: z.object({ averageRunTime: z.object({
value: z.number(), value: z.number(),
deviation: z.number(), deviation: z.union([z.null(), z.number()]),
unit: z.literal('time'), unit: z.literal('time'),
}), }),
} as const; } as const;

View File

@@ -34,15 +34,15 @@ describe('InsightsController', () => {
// ASSERT // ASSERT
expect(response).toEqual({ expect(response).toEqual({
total: { deviation: 0, unit: 'count', value: 0 }, total: { deviation: null, unit: 'count', value: 0 },
failed: { deviation: 0, unit: 'count', value: 0 }, failed: { deviation: null, unit: 'count', value: 0 },
failureRate: { deviation: 0, unit: 'ratio', value: 0 }, failureRate: { deviation: null, unit: 'ratio', value: 0 },
averageRunTime: { deviation: 0, unit: 'time', value: 0 }, averageRunTime: { deviation: null, unit: 'time', value: 0 },
timeSaved: { deviation: 0, unit: 'time', value: 0 }, timeSaved: { deviation: null, unit: 'time', value: 0 },
}); });
}); });
it('should return the insights summary with deviation = current if insights exist only for current period', async () => { it('should return the insights summary with null deviation if insights exist only for current period', async () => {
// ARRANGE // ARRANGE
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates.mockResolvedValue([ insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates.mockResolvedValue([
{ period: 'current', type: TypeToNumber.success, total_value: 20 }, { period: 'current', type: TypeToNumber.success, total_value: 20 },
@@ -56,11 +56,11 @@ describe('InsightsController', () => {
// ASSERT // ASSERT
expect(response).toEqual({ expect(response).toEqual({
total: { deviation: 30, unit: 'count', value: 30 }, total: { deviation: null, unit: 'count', value: 30 },
failed: { deviation: 10, unit: 'count', value: 10 }, failed: { deviation: null, unit: 'count', value: 10 },
failureRate: { deviation: 0.33, unit: 'ratio', value: 0.33 }, failureRate: { deviation: null, unit: 'ratio', value: 0.33 },
averageRunTime: { deviation: 10, unit: 'time', value: 10 }, averageRunTime: { deviation: null, unit: 'time', value: 10 },
timeSaved: { deviation: 10, unit: 'time', value: 10 }, timeSaved: { deviation: null, unit: 'time', value: 10 },
}); });
}); });

View File

@@ -784,4 +784,21 @@ describe('getInsightsSummary', () => {
total: { deviation: 3, unit: 'count', value: 4 }, total: { deviation: 3, unit: 'count', value: 4 },
}); });
}); });
test('no data for previous period should return null deviation', async () => {
// ARRANGE
// last 7 days
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
// ACT
const summary = await insightsService.getInsightsSummary();
// ASSERT
expect(Object.values(summary).map((v) => v.deviation)).toEqual([null, null, null, null, null]);
});
}); });

View File

@@ -265,32 +265,36 @@ export class InsightsService {
const currentTimeSaved = getValueByType('current', 'time_saved_min'); const currentTimeSaved = getValueByType('current', 'time_saved_min');
const previousTimeSaved = getValueByType('previous', 'time_saved_min'); const previousTimeSaved = getValueByType('previous', 'time_saved_min');
// If the previous period has no executions, we discard deviation
const getDeviation = (current: number, previous: number) =>
previousTotal === 0 ? null : current - previous;
// Return the formatted result // Return the formatted result
const result: InsightsSummary = { const result: InsightsSummary = {
averageRunTime: { averageRunTime: {
value: currentAvgRuntime, value: currentAvgRuntime,
unit: 'time', unit: 'time',
deviation: currentAvgRuntime - previousAvgRuntime, deviation: getDeviation(currentAvgRuntime, previousAvgRuntime),
}, },
failed: { failed: {
value: currentFailures, value: currentFailures,
unit: 'count', unit: 'count',
deviation: currentFailures - previousFailures, deviation: getDeviation(currentFailures, previousFailures),
}, },
failureRate: { failureRate: {
value: currentFailureRate, value: currentFailureRate,
unit: 'ratio', unit: 'ratio',
deviation: currentFailureRate - previousFailureRate, deviation: getDeviation(currentFailureRate, previousFailureRate),
}, },
timeSaved: { timeSaved: {
value: currentTimeSaved, value: currentTimeSaved,
unit: 'time', unit: 'time',
deviation: currentTimeSaved - previousTimeSaved, deviation: getDeviation(currentTimeSaved, previousTimeSaved),
}, },
total: { total: {
value: currentTotal, value: currentTotal,
unit: 'count', unit: 'count',
deviation: currentTotal - previousTotal, deviation: getDeviation(currentTotal, previousTotal),
}, },
}; };

View File

@@ -44,6 +44,15 @@ describe('InsightsSummary', () => {
{ id: 'averageRunTime', value: 2.5, deviation: 0.5, unit: 's' }, { id: 'averageRunTime', value: 2.5, deviation: 0.5, unit: 's' },
], ],
], ],
[
[
{ id: 'total', value: 525, deviation: null, unit: '' },
{ id: 'failed', value: 14, deviation: null, unit: '' },
{ id: 'failureRate', value: 1.9, deviation: null, unit: '%' },
{ id: 'timeSaved', value: 55.55555555555556, deviation: null, unit: 'h' },
{ id: 'averageRunTime', value: 2.5, deviation: null, unit: 's' },
],
],
])('should render the summary correctly', (summary) => { ])('should render the summary correctly', (summary) => {
const { html } = renderComponent({ const { html } = renderComponent({
props: { props: {

View File

@@ -76,7 +76,7 @@ const getImpactStyle = (id: keyof InsightsSummary, value: number) => {
<em <em
>{{ smartDecimal(value) }} <i>{{ unit }}</i></em >{{ smartDecimal(value) }} <i>{{ unit }}</i></em
> >
<small :class="getImpactStyle(id, deviation)"> <small v-if="deviation !== null" :class="getImpactStyle(id, deviation)">
<N8nIcon <N8nIcon
:class="[$style.icon, getImpactStyle(id, deviation)]" :class="[$style.icon, getImpactStyle(id, deviation)]"
:icon="deviation === 0 ? 'caret-right' : deviation > 0 ? 'caret-up' : 'caret-down'" :icon="deviation === 0 ? 'caret-right' : deviation > 0 ? 'caret-up' : 'caret-down'"

View File

@@ -73,3 +73,26 @@ exports[`InsightsSummary > should render the summary correctly 4`] = `
</ul> </ul>
</div>" </div>"
`; `;
exports[`InsightsSummary > should render the summary correctly 5`] = `
"<div class="insights">
<h3 class="n8n-heading text-light size-small bold mb-xs mb-xs">Production executions for the last 7 days</h3>
<ul data-test-id="insights-summary-tabs">
<li data-test-id="insights-summary-tab-total">
<p><strong>Total</strong><span><em>525 <i></i></em><!--v-if--></span></p>
</li>
<li data-test-id="insights-summary-tab-failed">
<p><strong>Failed</strong><span><em>14 <i></i></em><!--v-if--></span></p>
</li>
<li data-test-id="insights-summary-tab-failureRate">
<p><strong>Failure rate</strong><span><em>1.9 <i>%</i></em><!--v-if--></span></p>
</li>
<li data-test-id="insights-summary-tab-timeSaved">
<p><strong>Time saved</strong><span><em>55.56 <i>h</i></em><!--v-if--></span></p>
</li>
<li data-test-id="insights-summary-tab-averageRunTime">
<p><strong>Avg. run time</strong><span><em>2.5 <i>s</i></em><!--v-if--></span></p>
</li>
</ul>
</div>"
`;

View File

@@ -7,7 +7,7 @@ export type InsightsSummaryDisplay = Array<
[K in keyof InsightsDisplayUnits]: { [K in keyof InsightsDisplayUnits]: {
id: K; id: K;
value: number; value: number;
deviation: number; deviation: number | null;
unit: InsightsDisplayUnits[K]; unit: InsightsDisplayUnits[K];
}; };
}[keyof InsightsDisplayUnits] }[keyof InsightsDisplayUnits]

View File

@@ -16,7 +16,9 @@ export const transformInsightsSummary = (data: InsightsSummary | null): Insights
? INSIGHTS_SUMMARY_ORDER.map((key) => ({ ? INSIGHTS_SUMMARY_ORDER.map((key) => ({
id: key, id: key,
value: transformInsightsValues[key]?.(data[key].value) ?? data[key].value, value: transformInsightsValues[key]?.(data[key].value) ?? data[key].value,
deviation: transformInsightsValues[key]?.(data[key].deviation) ?? data[key].deviation, deviation: data[key].deviation
? (transformInsightsValues[key]?.(data[key].deviation) ?? data[key].deviation)
: null,
unit: INSIGHTS_UNIT_MAPPING[key], unit: INSIGHTS_UNIT_MAPPING[key],
})) }))
: []; : [];