mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
857 lines
25 KiB
TypeScript
857 lines
25 KiB
TypeScript
import type { InsightsDateRange } from '@n8n/api-types';
|
|
import type { LicenseState } from '@n8n/backend-common';
|
|
import type { Project } from '@n8n/db';
|
|
import type { WorkflowEntity } from '@n8n/db';
|
|
import type { IWorkflowDb } from '@n8n/db';
|
|
import type { WorkflowExecuteAfterContext } from '@n8n/decorators';
|
|
import { Container } from '@n8n/di';
|
|
import { mock } from 'jest-mock-extended';
|
|
import { DateTime } from 'luxon';
|
|
import type { IRun } from 'n8n-workflow';
|
|
|
|
import { mockLogger } from '@test/mocking';
|
|
import { createTeamProject } from '@test-integration/db/projects';
|
|
import { createWorkflow } from '@test-integration/db/workflows';
|
|
import * as testDb from '@test-integration/test-db';
|
|
|
|
import {
|
|
createCompactedInsightsEvent,
|
|
createMetadata,
|
|
createRawInsightsEvents,
|
|
} from '../database/entities/__tests__/db-utils';
|
|
import type { InsightsRaw } from '../database/entities/insights-raw';
|
|
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
|
|
import { InsightsCollectionService } from '../insights-collection.service';
|
|
import { InsightsCompactionService } from '../insights-compaction.service';
|
|
import type { InsightsPruningService } from '../insights-pruning.service';
|
|
import { InsightsConfig } from '../insights.config';
|
|
import { InsightsService } from '../insights.service';
|
|
|
|
// Initialize DB once for all tests
|
|
beforeAll(async () => {
|
|
await testDb.init();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await testDb.truncate([
|
|
'InsightsRaw',
|
|
'InsightsByPeriod',
|
|
'InsightsMetadata',
|
|
'WorkflowEntity',
|
|
'Project',
|
|
]);
|
|
});
|
|
|
|
// Terminate DB once after all tests complete
|
|
afterAll(async () => {
|
|
await testDb.terminate();
|
|
});
|
|
|
|
describe('getInsightsSummary', () => {
|
|
let insightsService: InsightsService;
|
|
beforeAll(async () => {
|
|
insightsService = Container.get(InsightsService);
|
|
});
|
|
|
|
let project: Project;
|
|
let workflow: IWorkflowDb & WorkflowEntity;
|
|
|
|
beforeEach(async () => {
|
|
project = await createTeamProject();
|
|
workflow = await createWorkflow({}, project);
|
|
});
|
|
|
|
test('compacted data are summarized correctly', async () => {
|
|
// ARRANGE
|
|
// last 6 days
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ day: 2 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'failure',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
// last 12 days
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'runtime_ms',
|
|
value: 123,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
//Outside range should not be taken into account
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'runtime_ms',
|
|
value: 123,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 13 }),
|
|
});
|
|
|
|
// ACT
|
|
const summary = await insightsService.getInsightsSummary({ periodLengthInDays: 6 });
|
|
|
|
// ASSERT
|
|
expect(summary).toEqual({
|
|
averageRunTime: { deviation: -123, unit: 'millisecond', value: 0 },
|
|
failed: { deviation: 1, unit: 'count', value: 1 },
|
|
failureRate: { deviation: 0.333, unit: 'ratio', value: 0.333 },
|
|
timeSaved: { deviation: 0, unit: 'minute', value: 0 },
|
|
total: { deviation: 2, unit: 'count', value: 3 },
|
|
});
|
|
});
|
|
|
|
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({ periodLengthInDays: 7 });
|
|
|
|
// ASSERT
|
|
expect(Object.values(summary).map((v) => v.deviation)).toEqual([null, null, null, null, null]);
|
|
});
|
|
|
|
test('mixed period data are summarized correctly', async () => {
|
|
// ARRANGE
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ day: 1 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'failure',
|
|
value: 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 2,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.utc().minus({ day: 10 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 3,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ day: 11 }),
|
|
});
|
|
|
|
// ACT
|
|
const summary = await insightsService.getInsightsSummary({ periodLengthInDays: 7 });
|
|
|
|
// ASSERT
|
|
expect(summary).toEqual({
|
|
averageRunTime: { deviation: 0, unit: 'millisecond', value: 0 },
|
|
failed: { deviation: 2, unit: 'count', value: 2 },
|
|
failureRate: { deviation: 0.5, unit: 'ratio', value: 0.5 },
|
|
timeSaved: { deviation: 0, unit: 'minute', value: 0 },
|
|
total: { deviation: -1, unit: 'count', value: 4 },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getInsightsByWorkflow', () => {
|
|
let insightsService: InsightsService;
|
|
beforeAll(async () => {
|
|
insightsService = Container.get(InsightsService);
|
|
});
|
|
|
|
let project: Project;
|
|
let workflow1: IWorkflowDb & WorkflowEntity;
|
|
let workflow2: IWorkflowDb & WorkflowEntity;
|
|
let workflow3: IWorkflowDb & WorkflowEntity;
|
|
|
|
beforeEach(async () => {
|
|
project = await createTeamProject();
|
|
workflow1 = await createWorkflow({}, project);
|
|
workflow2 = await createWorkflow({}, project);
|
|
workflow3 = await createWorkflow({}, project);
|
|
});
|
|
|
|
test('compacted data are are grouped by workflow correctly', async () => {
|
|
// ARRANGE
|
|
for (const workflow of [workflow1, workflow2]) {
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: workflow === workflow1 ? 1 : 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ day: 2 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'failure',
|
|
value: 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
// last 14 days
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'runtime_ms',
|
|
value: 123,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
|
|
// Barely in range insight (should be included)
|
|
// 1 hour before 14 days ago
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.utc().minus({ days: 13, hours: 23 }),
|
|
});
|
|
|
|
// Out of date range insight (should not be included)
|
|
// 14 days ago
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 14 }),
|
|
});
|
|
}
|
|
|
|
// ACT
|
|
const byWorkflow = await insightsService.getInsightsByWorkflow({
|
|
maxAgeInDays: 14,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(byWorkflow.count).toEqual(2);
|
|
expect(byWorkflow.data).toHaveLength(2);
|
|
|
|
// expect first workflow to be workflow 2, because it has a bigger total (default sorting)
|
|
expect(byWorkflow.data[0]).toMatchObject({
|
|
workflowId: workflow2.id,
|
|
workflowName: workflow2.name,
|
|
projectId: project.id,
|
|
projectName: project.name,
|
|
total: 7,
|
|
failed: 2,
|
|
runTime: 123,
|
|
succeeded: 5,
|
|
timeSaved: 0,
|
|
});
|
|
expect(byWorkflow.data[0].failureRate).toBeCloseTo(2 / 7);
|
|
expect(byWorkflow.data[0].averageRunTime).toBeCloseTo(123 / 7);
|
|
|
|
expect(byWorkflow.data[1]).toMatchObject({
|
|
workflowId: workflow1.id,
|
|
workflowName: workflow1.name,
|
|
projectId: project.id,
|
|
projectName: project.name,
|
|
total: 6,
|
|
failed: 2,
|
|
runTime: 123,
|
|
succeeded: 4,
|
|
timeSaved: 0,
|
|
});
|
|
expect(byWorkflow.data[1].failureRate).toBeCloseTo(2 / 6);
|
|
expect(byWorkflow.data[1].averageRunTime).toBeCloseTo(123 / 6);
|
|
});
|
|
|
|
test('compacted data are grouped by workflow correctly with sorting', async () => {
|
|
// ARRANGE
|
|
for (const workflow of [workflow1, workflow2]) {
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: workflow === workflow1 ? 1 : 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'failure',
|
|
value: 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'runtime_ms',
|
|
value: workflow === workflow1 ? 2 : 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
}
|
|
|
|
// ACT
|
|
const byWorkflow = await insightsService.getInsightsByWorkflow({
|
|
maxAgeInDays: 14,
|
|
sortBy: 'runTime:desc',
|
|
});
|
|
|
|
// ASSERT
|
|
expect(byWorkflow.count).toEqual(2);
|
|
expect(byWorkflow.data).toHaveLength(2);
|
|
expect(byWorkflow.data[0].workflowId).toEqual(workflow1.id);
|
|
});
|
|
|
|
test('compacted data are grouped by workflow correctly with pagination', async () => {
|
|
// ARRANGE
|
|
for (const workflow of [workflow1, workflow2, workflow3]) {
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: workflow === workflow1 ? 1 : workflow === workflow2 ? 2 : 3,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
}
|
|
|
|
// ACT
|
|
const byWorkflow = await insightsService.getInsightsByWorkflow({
|
|
maxAgeInDays: 14,
|
|
sortBy: 'succeeded:desc',
|
|
skip: 1,
|
|
take: 1,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(byWorkflow.count).toEqual(3);
|
|
expect(byWorkflow.data).toHaveLength(1);
|
|
expect(byWorkflow.data[0].workflowId).toEqual(workflow2.id);
|
|
});
|
|
|
|
test('compacted data are grouped by workflow correctly even with 0 data (check division by 0)', async () => {
|
|
// ACT
|
|
const byWorkflow = await insightsService.getInsightsByWorkflow({
|
|
maxAgeInDays: 14,
|
|
});
|
|
|
|
// ASSERT
|
|
expect(byWorkflow.count).toEqual(0);
|
|
expect(byWorkflow.data).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getInsightsByTime', () => {
|
|
let insightsService: InsightsService;
|
|
beforeAll(async () => {
|
|
insightsService = Container.get(InsightsService);
|
|
});
|
|
|
|
let project: Project;
|
|
let workflow1: IWorkflowDb & WorkflowEntity;
|
|
let workflow2: IWorkflowDb & WorkflowEntity;
|
|
|
|
beforeEach(async () => {
|
|
project = await createTeamProject();
|
|
workflow1 = await createWorkflow({}, project);
|
|
workflow2 = await createWorkflow({}, project);
|
|
});
|
|
|
|
test('returns empty array when no insights exist', async () => {
|
|
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
|
|
expect(byTime).toEqual([]);
|
|
});
|
|
|
|
test('returns empty array when no insights in the time range exists', async () => {
|
|
await createCompactedInsightsEvent(workflow1, {
|
|
type: 'success',
|
|
value: 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 30 }),
|
|
});
|
|
|
|
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
|
|
expect(byTime).toEqual([]);
|
|
});
|
|
|
|
test('compacted data are are grouped by time correctly', async () => {
|
|
// ARRANGE
|
|
for (const workflow of [workflow1, workflow2]) {
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: workflow === workflow1 ? 1 : 2,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
// Check that hourly data is grouped together with the previous daily data
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'failure',
|
|
value: 2,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.utc(),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ day: 2 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'runtime_ms',
|
|
value: workflow === workflow1 ? 10 : 20,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 10 }),
|
|
});
|
|
|
|
// Barely in range insight (should be included)
|
|
// 1 hour before 14 days ago
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: workflow === workflow1 ? 'success' : 'failure',
|
|
value: 1,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.utc().minus({ days: 13, hours: 23 }),
|
|
});
|
|
|
|
// Out of date range insight (should not be included)
|
|
// 14 days ago
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'day',
|
|
periodStart: DateTime.utc().minus({ days: 14 }),
|
|
});
|
|
}
|
|
|
|
// ACT
|
|
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
|
|
|
|
// ASSERT
|
|
expect(byTime).toHaveLength(4);
|
|
|
|
// expect date to be sorted by oldest first
|
|
expect(byTime[0].date).toEqual(DateTime.utc().minus({ days: 14 }).startOf('day').toISO());
|
|
expect(byTime[1].date).toEqual(DateTime.utc().minus({ days: 10 }).startOf('day').toISO());
|
|
expect(byTime[2].date).toEqual(DateTime.utc().minus({ days: 2 }).startOf('day').toISO());
|
|
expect(byTime[3].date).toEqual(DateTime.utc().startOf('day').toISO());
|
|
|
|
expect(byTime[0].values).toEqual({
|
|
total: 2,
|
|
succeeded: 1,
|
|
failed: 1,
|
|
failureRate: 0.5,
|
|
averageRunTime: 0,
|
|
timeSaved: 0,
|
|
});
|
|
|
|
expect(byTime[1].values).toEqual({
|
|
total: 2,
|
|
succeeded: 2,
|
|
failed: 0,
|
|
failureRate: 0,
|
|
averageRunTime: 15,
|
|
timeSaved: 0,
|
|
});
|
|
|
|
expect(byTime[2].values).toEqual({
|
|
total: 2,
|
|
succeeded: 2,
|
|
failed: 0,
|
|
failureRate: 0,
|
|
averageRunTime: 0,
|
|
timeSaved: 0,
|
|
});
|
|
|
|
expect(byTime[3].values).toEqual({
|
|
total: 7,
|
|
succeeded: 3,
|
|
failed: 4,
|
|
failureRate: 4 / 7,
|
|
averageRunTime: 0,
|
|
timeSaved: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getAvailableDateRanges', () => {
|
|
let insightsService: InsightsService;
|
|
let licenseMock: jest.Mocked<LicenseState>;
|
|
|
|
beforeAll(() => {
|
|
licenseMock = mock<LicenseState>();
|
|
insightsService = new InsightsService(
|
|
mock<InsightsByPeriodRepository>(),
|
|
mock<InsightsCompactionService>(),
|
|
mock<InsightsCollectionService>(),
|
|
mock<InsightsPruningService>(),
|
|
licenseMock,
|
|
mockLogger(),
|
|
);
|
|
});
|
|
|
|
test('returns correct ranges when hourly data is enabled and max history is unlimited', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
const result = insightsService.getAvailableDateRanges();
|
|
|
|
expect(result).toEqual([
|
|
{ key: 'day', licensed: true, granularity: 'hour' },
|
|
{ key: 'week', licensed: true, granularity: 'day' },
|
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
|
{ key: 'month', licensed: true, granularity: 'day' },
|
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
|
{ key: '6months', licensed: true, granularity: 'week' },
|
|
{ key: 'year', licensed: true, granularity: 'week' },
|
|
]);
|
|
});
|
|
|
|
test('returns correct ranges when hourly data is enabled and max history is 365 days', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
const result = insightsService.getAvailableDateRanges();
|
|
|
|
expect(result).toEqual([
|
|
{ key: 'day', licensed: true, granularity: 'hour' },
|
|
{ key: 'week', licensed: true, granularity: 'day' },
|
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
|
{ key: 'month', licensed: true, granularity: 'day' },
|
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
|
{ key: '6months', licensed: true, granularity: 'week' },
|
|
{ key: 'year', licensed: true, granularity: 'week' },
|
|
]);
|
|
});
|
|
|
|
test('returns correct ranges when hourly data is disabled and max history is 30 days', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
|
|
|
const result = insightsService.getAvailableDateRanges();
|
|
|
|
expect(result).toEqual([
|
|
{ key: 'day', licensed: false, granularity: 'hour' },
|
|
{ key: 'week', licensed: true, granularity: 'day' },
|
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
|
{ key: 'month', licensed: true, granularity: 'day' },
|
|
{ key: 'quarter', licensed: false, granularity: 'week' },
|
|
{ key: '6months', licensed: false, granularity: 'week' },
|
|
{ key: 'year', licensed: false, granularity: 'week' },
|
|
]);
|
|
});
|
|
|
|
test('returns correct ranges when max history is less than 7 days', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(5);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
|
|
|
const result = insightsService.getAvailableDateRanges();
|
|
|
|
expect(result).toEqual([
|
|
{ key: 'day', licensed: false, granularity: 'hour' },
|
|
{ key: 'week', licensed: false, granularity: 'day' },
|
|
{ key: '2weeks', licensed: false, granularity: 'day' },
|
|
{ key: 'month', licensed: false, granularity: 'day' },
|
|
{ key: 'quarter', licensed: false, granularity: 'week' },
|
|
{ key: '6months', licensed: false, granularity: 'week' },
|
|
{ key: 'year', licensed: false, granularity: 'week' },
|
|
]);
|
|
});
|
|
|
|
test('returns correct ranges when max history is 90 days and hourly data is enabled', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
const result = insightsService.getAvailableDateRanges();
|
|
|
|
expect(result).toEqual([
|
|
{ key: 'day', licensed: true, granularity: 'hour' },
|
|
{ key: 'week', licensed: true, granularity: 'day' },
|
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
|
{ key: 'month', licensed: true, granularity: 'day' },
|
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
|
{ key: '6months', licensed: false, granularity: 'week' },
|
|
{ key: 'year', licensed: false, granularity: 'week' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('getMaxAgeInDaysAndGranularity', () => {
|
|
let insightsService: InsightsService;
|
|
let licenseMock: jest.Mocked<LicenseState>;
|
|
|
|
beforeAll(() => {
|
|
licenseMock = mock<LicenseState>();
|
|
insightsService = new InsightsService(
|
|
mock<InsightsByPeriodRepository>(),
|
|
mock<InsightsCompactionService>(),
|
|
mock<InsightsCollectionService>(),
|
|
mock<InsightsPruningService>(),
|
|
licenseMock,
|
|
mockLogger(),
|
|
);
|
|
});
|
|
|
|
test('returns correct maxAgeInDays and granularity for a valid licensed date range', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
const result = insightsService.getMaxAgeInDaysAndGranularity('month');
|
|
|
|
expect(result).toEqual({
|
|
key: 'month',
|
|
licensed: true,
|
|
granularity: 'day',
|
|
maxAgeInDays: 30,
|
|
});
|
|
});
|
|
|
|
test('throws an error if the date range is not available', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
expect(() => {
|
|
insightsService.getMaxAgeInDaysAndGranularity('invalidKey' as InsightsDateRange['key']);
|
|
}).toThrowError('The selected date range is not available');
|
|
});
|
|
|
|
test('throws an error if the date range is not licensed', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
|
|
|
expect(() => {
|
|
insightsService.getMaxAgeInDaysAndGranularity('year');
|
|
}).toThrowError('The selected date range exceeds the maximum history allowed by your license.');
|
|
});
|
|
|
|
test('returns correct maxAgeInDays and granularity for a valid date range with hourly data disabled', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
|
|
|
const result = insightsService.getMaxAgeInDaysAndGranularity('quarter');
|
|
|
|
expect(result).toEqual({
|
|
key: 'quarter',
|
|
licensed: true,
|
|
granularity: 'week',
|
|
maxAgeInDays: 90,
|
|
});
|
|
});
|
|
|
|
test('returns correct maxAgeInDays and granularity for a valid date range with unlimited history', () => {
|
|
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
|
|
|
const result = insightsService.getMaxAgeInDaysAndGranularity('day');
|
|
|
|
expect(result).toEqual({
|
|
key: 'day',
|
|
licensed: true,
|
|
granularity: 'hour',
|
|
maxAgeInDays: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('shutdown', () => {
|
|
let insightsService: InsightsService;
|
|
|
|
const mockCollectionService = mock<InsightsCollectionService>({
|
|
shutdown: jest.fn().mockResolvedValue(undefined),
|
|
stopFlushingTimer: jest.fn(),
|
|
});
|
|
|
|
const mockCompactionService = mock<InsightsCompactionService>({
|
|
stopCompactionTimer: jest.fn(),
|
|
});
|
|
|
|
const mockPruningService = mock<InsightsPruningService>({
|
|
stopPruningTimer: jest.fn(),
|
|
});
|
|
|
|
beforeAll(() => {
|
|
insightsService = new InsightsService(
|
|
mock<InsightsByPeriodRepository>(),
|
|
mockCompactionService,
|
|
mockCollectionService,
|
|
mockPruningService,
|
|
mock<LicenseState>(),
|
|
mockLogger(),
|
|
);
|
|
});
|
|
|
|
test('shutdown stops timers and shuts down services', async () => {
|
|
// ACT
|
|
await insightsService.shutdown();
|
|
|
|
// ASSERT
|
|
expect(mockCollectionService.shutdown).toHaveBeenCalled();
|
|
expect(mockCompactionService.stopCompactionTimer).toHaveBeenCalled();
|
|
expect(mockPruningService.stopPruningTimer).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('timers', () => {
|
|
let insightsService: InsightsService;
|
|
|
|
const mockCollectionService = mock<InsightsCollectionService>({
|
|
startFlushingTimer: jest.fn(),
|
|
stopFlushingTimer: jest.fn(),
|
|
});
|
|
|
|
const mockCompactionService = mock<InsightsCompactionService>({
|
|
startCompactionTimer: jest.fn(),
|
|
stopCompactionTimer: jest.fn(),
|
|
});
|
|
|
|
const mockPruningService = mock<InsightsPruningService>({
|
|
startPruningTimer: jest.fn(),
|
|
stopPruningTimer: jest.fn(),
|
|
isPruningEnabled: false,
|
|
});
|
|
|
|
const mockedLogger = mockLogger();
|
|
const mockedConfig = mock<InsightsConfig>({
|
|
maxAgeDays: -1,
|
|
});
|
|
|
|
beforeAll(() => {
|
|
insightsService = new InsightsService(
|
|
mock<InsightsByPeriodRepository>(),
|
|
mockCompactionService,
|
|
mockCollectionService,
|
|
mockPruningService,
|
|
mock<LicenseState>(),
|
|
mockedLogger,
|
|
);
|
|
});
|
|
|
|
test('startTimers starts timers except pruning', () => {
|
|
// ACT
|
|
insightsService.startTimers();
|
|
|
|
// ASSERT
|
|
expect(mockCompactionService.startCompactionTimer).toHaveBeenCalled();
|
|
expect(mockCollectionService.startFlushingTimer).toHaveBeenCalled();
|
|
expect(mockPruningService.startPruningTimer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('startTimers starts pruning timer', () => {
|
|
// ARRANGE
|
|
mockedConfig.maxAgeDays = 30;
|
|
Object.defineProperty(mockPruningService, 'isPruningEnabled', { value: true });
|
|
|
|
// ACT
|
|
insightsService.startTimers();
|
|
|
|
// ASSERT
|
|
expect(mockPruningService.startPruningTimer).toHaveBeenCalled();
|
|
});
|
|
|
|
test('stopTimers stops timers', () => {
|
|
// ACT
|
|
insightsService.stopTimers();
|
|
|
|
// ASSERT
|
|
expect(mockCompactionService.stopCompactionTimer).toHaveBeenCalled();
|
|
expect(mockCollectionService.stopFlushingTimer).toHaveBeenCalled();
|
|
expect(mockPruningService.stopPruningTimer).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('legacy sqlite (without pooling) handles concurrent insights db process without throwing', () => {
|
|
let initialFlushBatchSize: number;
|
|
let insightsConfig: InsightsConfig;
|
|
beforeAll(() => {
|
|
insightsConfig = Container.get(InsightsConfig);
|
|
initialFlushBatchSize = insightsConfig.flushBatchSize;
|
|
|
|
insightsConfig.flushBatchSize = 50;
|
|
});
|
|
|
|
afterAll(() => {
|
|
insightsConfig.flushBatchSize = initialFlushBatchSize;
|
|
});
|
|
|
|
test('should handle concurrent flush and compaction without error', async () => {
|
|
const insightsCollectionService = Container.get(InsightsCollectionService);
|
|
const insightsCompactionService = Container.get(InsightsCompactionService);
|
|
|
|
const project = await createTeamProject();
|
|
const workflow = await createWorkflow({}, project);
|
|
await createMetadata(workflow);
|
|
|
|
const ctx = mock<WorkflowExecuteAfterContext>({ workflow });
|
|
const startedAt = DateTime.utc();
|
|
const stoppedAt = startedAt.plus({ seconds: 5 });
|
|
ctx.runData = mock<IRun>({
|
|
mode: 'webhook',
|
|
status: 'success',
|
|
startedAt: startedAt.toJSDate(),
|
|
stoppedAt: stoppedAt.toJSDate(),
|
|
});
|
|
|
|
// Create test data
|
|
const rawInsights = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
rawInsights.push({
|
|
type: 'success' as InsightsRaw['type'],
|
|
value: 1,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.now().minus({ day: 91, hour: i + 1 }),
|
|
});
|
|
}
|
|
// Create raw insights events to be compacted
|
|
await createRawInsightsEvents(workflow, rawInsights);
|
|
|
|
//
|
|
for (let i = 0; i < 100; i++) {
|
|
await createCompactedInsightsEvent(workflow, {
|
|
type: 'success',
|
|
value: 1,
|
|
periodUnit: 'hour',
|
|
periodStart: DateTime.now().minus({ day: 91, hour: i + 1 }),
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
await insightsCollectionService.handleWorkflowExecuteAfter(ctx);
|
|
}
|
|
|
|
// ACT
|
|
const promises = [
|
|
insightsCollectionService.flushEvents(),
|
|
insightsCollectionService.flushEvents(),
|
|
insightsCompactionService.compactRawToHour(),
|
|
insightsCompactionService.compactHourToDay(),
|
|
];
|
|
await expect(Promise.all(promises)).resolves.toBeDefined();
|
|
});
|
|
});
|