mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Allow filtering insight by projectId (#19552)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Irénée <ireneea@users.noreply.github.com> Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
@@ -25,10 +25,15 @@ afterAll(async () => {
|
||||
describe('InsightsController', () => {
|
||||
const insightsByPeriodRepository = mockInstance(InsightsByPeriodRepository);
|
||||
let controller: InsightsController;
|
||||
|
||||
beforeAll(async () => {
|
||||
controller = Container.get(InsightsController);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getInsightsSummary', () => {
|
||||
it('should return default insights if no data', async () => {
|
||||
// ARRANGE
|
||||
@@ -41,6 +46,10 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates,
|
||||
).toHaveBeenCalledWith({ periodLengthInDays: 7 });
|
||||
|
||||
expect(response).toEqual({
|
||||
total: { deviation: null, unit: 'count', value: 0 },
|
||||
failed: { deviation: null, unit: 'count', value: 0 },
|
||||
@@ -66,6 +75,10 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates,
|
||||
).toHaveBeenCalledWith({ periodLengthInDays: 7 });
|
||||
|
||||
expect(response).toEqual({
|
||||
total: { deviation: null, unit: 'count', value: 30 },
|
||||
failed: { deviation: null, unit: 'count', value: 10 },
|
||||
@@ -95,6 +108,46 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(
|
||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates,
|
||||
).toHaveBeenCalledWith({ periodLengthInDays: 7 });
|
||||
|
||||
expect(response).toEqual({
|
||||
total: { deviation: 10, unit: 'count', value: 30 },
|
||||
failed: { deviation: 6, unit: 'count', value: 10 },
|
||||
failureRate: { deviation: 0.333 - 0.2, unit: 'ratio', value: 0.333 },
|
||||
averageRunTime: { deviation: 300 / 30 - 40 / 20, unit: 'millisecond', value: 10 },
|
||||
timeSaved: { deviation: 5, unit: 'minute', value: 10 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the query filters when provided', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates.mockResolvedValue([
|
||||
{ period: 'previous', type: TypeToNumber.success, total_value: 16 },
|
||||
{ period: 'previous', type: TypeToNumber.failure, total_value: 4 },
|
||||
{ period: 'previous', type: TypeToNumber.runtime_ms, total_value: 40 },
|
||||
{ period: 'previous', type: TypeToNumber.time_saved_min, total_value: 5 },
|
||||
{ period: 'current', type: TypeToNumber.success, total_value: 20 },
|
||||
{ period: 'current', type: TypeToNumber.failure, total_value: 10 },
|
||||
{ period: 'current', type: TypeToNumber.runtime_ms, total_value: 300 },
|
||||
{ period: 'current', type: TypeToNumber.time_saved_min, total_value: 10 },
|
||||
]);
|
||||
|
||||
// ACT
|
||||
const response = await controller.getInsightsSummary(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{ dateRange: 'month', projectId: 'test-project' },
|
||||
);
|
||||
|
||||
expect(
|
||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates,
|
||||
).toHaveBeenCalledWith({
|
||||
periodLengthInDays: 30,
|
||||
projectId: 'test-project',
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
total: { deviation: 10, unit: 'count', value: 30 },
|
||||
failed: { deviation: 6, unit: 'count', value: 10 },
|
||||
@@ -105,7 +158,157 @@ describe('InsightsController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInsightsByWorkflow', () => {
|
||||
const mockRows = [
|
||||
{
|
||||
workflowId: 'workflow-1',
|
||||
workflowName: 'Workflow A',
|
||||
projectId: 'project-1',
|
||||
projectName: 'Project Alpha',
|
||||
total: 30664,
|
||||
succeeded: 30077,
|
||||
failed: 587,
|
||||
failureRate: 0.019142968953822073,
|
||||
runTime: 1587932583,
|
||||
averageRunTime: 51784.91335116097,
|
||||
timeSaved: 0,
|
||||
},
|
||||
{
|
||||
workflowId: 'workflow-2',
|
||||
workflowName: 'Workflow B',
|
||||
projectId: 'project-1',
|
||||
projectName: 'Project Alpha',
|
||||
total: 27332,
|
||||
succeeded: 27332,
|
||||
failed: 0,
|
||||
failureRate: 0,
|
||||
runTime: 1880,
|
||||
averageRunTime: 0.06878384311429826,
|
||||
timeSaved: 0,
|
||||
},
|
||||
{
|
||||
workflowId: 'workflow-3',
|
||||
workflowName: 'Workflow C',
|
||||
projectId: 'project-1',
|
||||
projectName: 'Project Alpha',
|
||||
total: 15167,
|
||||
succeeded: 14956,
|
||||
failed: 211,
|
||||
failureRate: 0.013911782158633876,
|
||||
runTime: 899930618,
|
||||
averageRunTime: 59334.78064218369,
|
||||
timeSaved: 0,
|
||||
},
|
||||
];
|
||||
|
||||
it('should return empty insights by workflow if no data', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByWorkflow.mockResolvedValue({ count: 0, rows: [] });
|
||||
|
||||
// ACT
|
||||
const response = await controller.getInsightsByWorkflow(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{
|
||||
skip: 0,
|
||||
take: 5,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'week',
|
||||
},
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByWorkflow).toHaveBeenCalledWith({
|
||||
maxAgeInDays: 7,
|
||||
skip: 0,
|
||||
take: 5,
|
||||
sortBy: 'total:desc',
|
||||
});
|
||||
|
||||
expect(response).toEqual({ count: 0, data: [] });
|
||||
});
|
||||
|
||||
it('should return insights by workflow', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByWorkflow.mockResolvedValue({
|
||||
count: mockRows.length,
|
||||
rows: mockRows,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const response = await controller.getInsightsByWorkflow(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{
|
||||
skip: 0,
|
||||
take: 5,
|
||||
sortBy: 'total:desc',
|
||||
dateRange: 'week',
|
||||
},
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByWorkflow).toHaveBeenCalledWith({
|
||||
maxAgeInDays: 7,
|
||||
skip: 0,
|
||||
take: 5,
|
||||
sortBy: 'total:desc',
|
||||
});
|
||||
|
||||
expect(response).toEqual({ count: 3, data: mockRows });
|
||||
});
|
||||
|
||||
it('should use the query filters when provided', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByWorkflow.mockResolvedValue({
|
||||
count: mockRows.length,
|
||||
rows: mockRows,
|
||||
});
|
||||
|
||||
// ACT
|
||||
const response = await controller.getInsightsByWorkflow(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{
|
||||
skip: 5,
|
||||
take: 10,
|
||||
sortBy: 'failureRate:asc',
|
||||
dateRange: 'month',
|
||||
projectId: 'test-project',
|
||||
},
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByWorkflow).toHaveBeenCalledWith({
|
||||
maxAgeInDays: 30,
|
||||
skip: 5,
|
||||
take: 10,
|
||||
sortBy: 'failureRate:asc',
|
||||
projectId: 'test-project',
|
||||
});
|
||||
|
||||
expect(response).toEqual({ count: 3, data: mockRows });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInsightsByTime', () => {
|
||||
const mockData = [
|
||||
{
|
||||
periodStart: '2023-10-01T00:00:00.000Z',
|
||||
succeeded: 10,
|
||||
timeSaved: 0,
|
||||
failed: 2,
|
||||
runTime: 10,
|
||||
},
|
||||
{
|
||||
periodStart: '2023-10-02T00:00:00.000Z',
|
||||
succeeded: 12,
|
||||
timeSaved: 0,
|
||||
failed: 4,
|
||||
runTime: 10,
|
||||
},
|
||||
];
|
||||
|
||||
it('should return insights by time with empty data', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByTime.mockResolvedValue([]);
|
||||
@@ -118,27 +321,16 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByTime).toHaveBeenCalledWith({
|
||||
insightTypes: ['time_saved_min', 'runtime_ms', 'success', 'failure'],
|
||||
maxAgeInDays: 7,
|
||||
periodUnit: 'day',
|
||||
});
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return insights by time with all data', async () => {
|
||||
it('should return insights by time', async () => {
|
||||
// ARRANGE
|
||||
const mockData = [
|
||||
{
|
||||
periodStart: '2023-10-01T00:00:00.000Z',
|
||||
succeeded: 10,
|
||||
timeSaved: 0,
|
||||
failed: 2,
|
||||
runTime: 10,
|
||||
},
|
||||
{
|
||||
periodStart: '2023-10-02T00:00:00.000Z',
|
||||
succeeded: 12,
|
||||
timeSaved: 0,
|
||||
failed: 4,
|
||||
runTime: 10,
|
||||
},
|
||||
];
|
||||
insightsByPeriodRepository.getInsightsByTime.mockResolvedValue(mockData);
|
||||
|
||||
// ACT
|
||||
@@ -149,6 +341,57 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByTime).toHaveBeenCalledWith({
|
||||
insightTypes: ['time_saved_min', 'runtime_ms', 'success', 'failure'],
|
||||
maxAgeInDays: 365,
|
||||
periodUnit: 'week',
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
date: '2023-10-01T00:00:00.000Z',
|
||||
values: {
|
||||
succeeded: 10,
|
||||
timeSaved: 0,
|
||||
failed: 2,
|
||||
averageRunTime: 10 / 12,
|
||||
failureRate: 2 / 12,
|
||||
total: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
date: '2023-10-02T00:00:00.000Z',
|
||||
values: {
|
||||
succeeded: 12,
|
||||
timeSaved: 0,
|
||||
failed: 4,
|
||||
averageRunTime: 10 / 16,
|
||||
failureRate: 4 / 16,
|
||||
total: 16,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the projectId query filters when provided', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByTime.mockResolvedValue(mockData);
|
||||
|
||||
// ACT
|
||||
const response = await controller.getInsightsByTime(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{ dateRange: 'month', projectId: 'test-project' },
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByTime).toHaveBeenCalledWith({
|
||||
insightTypes: ['time_saved_min', 'runtime_ms', 'success', 'failure'],
|
||||
maxAgeInDays: 30,
|
||||
periodUnit: 'day',
|
||||
projectId: 'test-project',
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
date: '2023-10-01T00:00:00.000Z',
|
||||
@@ -177,18 +420,19 @@ describe('InsightsController', () => {
|
||||
});
|
||||
|
||||
describe('getTimeSavedInsightsByTime', () => {
|
||||
const mockData = [
|
||||
{
|
||||
periodStart: '2023-10-01T00:00:00.000Z',
|
||||
timeSaved: 0,
|
||||
},
|
||||
{
|
||||
periodStart: '2023-10-02T00:00:00.000Z',
|
||||
timeSaved: 2,
|
||||
},
|
||||
];
|
||||
|
||||
it('should return insights by time with limited data', async () => {
|
||||
// ARRANGE
|
||||
const mockData = [
|
||||
{
|
||||
periodStart: '2023-10-01T00:00:00.000Z',
|
||||
timeSaved: 0,
|
||||
},
|
||||
{
|
||||
periodStart: '2023-10-02T00:00:00.000Z',
|
||||
timeSaved: 2,
|
||||
},
|
||||
];
|
||||
insightsByPeriodRepository.getInsightsByTime.mockResolvedValue(mockData);
|
||||
|
||||
// ACT
|
||||
@@ -199,6 +443,47 @@ describe('InsightsController', () => {
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByTime).toHaveBeenCalledWith({
|
||||
insightTypes: ['time_saved_min'],
|
||||
maxAgeInDays: 7,
|
||||
periodUnit: 'day',
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
date: '2023-10-01T00:00:00.000Z',
|
||||
values: {
|
||||
timeSaved: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
date: '2023-10-02T00:00:00.000Z',
|
||||
values: {
|
||||
timeSaved: 2,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the projectId query filters when provided', async () => {
|
||||
// ARRANGE
|
||||
insightsByPeriodRepository.getInsightsByTime.mockResolvedValue(mockData);
|
||||
|
||||
// ACT
|
||||
const response = await controller.getTimeSavedInsightsByTime(
|
||||
mock<AuthenticatedRequest>(),
|
||||
mock<Response>(),
|
||||
{ dateRange: 'month', projectId: 'test-project' },
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(insightsByPeriodRepository.getInsightsByTime).toHaveBeenCalledWith({
|
||||
insightTypes: ['time_saved_min'],
|
||||
maxAgeInDays: 30,
|
||||
periodUnit: 'day',
|
||||
projectId: 'test-project',
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
{
|
||||
date: '2023-10-01T00:00:00.000Z',
|
||||
|
||||
@@ -258,6 +258,93 @@ describe('getInsightsSummary', () => {
|
||||
total: { deviation: -1, unit: 'count', value: 4 },
|
||||
});
|
||||
});
|
||||
|
||||
test('filter by projectId', async () => {
|
||||
// ARRANGE
|
||||
const otherProject = await createTeamProject();
|
||||
const otherWorkflow = await createWorkflow({}, otherProject);
|
||||
|
||||
// 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(),
|
||||
});
|
||||
|
||||
await createCompactedInsightsEvent(otherWorkflow, {
|
||||
type: 'runtime_ms',
|
||||
value: 430,
|
||||
periodUnit: 'day',
|
||||
periodStart: DateTime.utc().minus({ day: 1 }),
|
||||
});
|
||||
await createCompactedInsightsEvent(otherWorkflow, {
|
||||
type: 'failure',
|
||||
value: 1,
|
||||
periodUnit: 'day',
|
||||
periodStart: DateTime.utc().minus({ day: 3 }),
|
||||
});
|
||||
|
||||
// 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 }),
|
||||
});
|
||||
await createCompactedInsightsEvent(otherWorkflow, {
|
||||
type: 'runtime_ms',
|
||||
value: 45,
|
||||
periodUnit: 'day',
|
||||
periodStart: DateTime.utc().minus({ days: 11 }),
|
||||
});
|
||||
//Outside range should not be taken into account
|
||||
await createCompactedInsightsEvent(workflow, {
|
||||
type: 'runtime_ms',
|
||||
value: 123,
|
||||
periodUnit: 'day',
|
||||
periodStart: DateTime.utc().minus({ days: 13 }),
|
||||
});
|
||||
await createCompactedInsightsEvent(otherWorkflow, {
|
||||
type: 'runtime_ms',
|
||||
value: 100,
|
||||
periodUnit: 'day',
|
||||
periodStart: DateTime.utc().minus({ days: 20 }),
|
||||
});
|
||||
|
||||
// ACT
|
||||
const summary = await insightsService.getInsightsSummary({
|
||||
periodLengthInDays: 6,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// 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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInsightsByWorkflow', () => {
|
||||
@@ -267,15 +354,20 @@ describe('getInsightsByWorkflow', () => {
|
||||
});
|
||||
|
||||
let project: Project;
|
||||
let project2: Project;
|
||||
let workflow1: IWorkflowDb & WorkflowEntity;
|
||||
let workflow2: IWorkflowDb & WorkflowEntity;
|
||||
let workflow3: IWorkflowDb & WorkflowEntity;
|
||||
let workflow4: IWorkflowDb & WorkflowEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
project = await createTeamProject();
|
||||
workflow1 = await createWorkflow({}, project);
|
||||
workflow2 = await createWorkflow({}, project);
|
||||
workflow3 = await createWorkflow({}, project);
|
||||
|
||||
project2 = await createTeamProject();
|
||||
workflow4 = await createWorkflow({}, project2);
|
||||
});
|
||||
|
||||
test('compacted data are are grouped by workflow correctly', async () => {
|
||||
@@ -431,6 +523,100 @@ describe('getInsightsByWorkflow', () => {
|
||||
expect(byWorkflow.data[0].workflowId).toEqual(workflow2.id);
|
||||
});
|
||||
|
||||
test('compacted data are grouped by workflow correctly with projectId filter', async () => {
|
||||
// ARRANGE
|
||||
for (const workflow of [workflow1, workflow2, workflow4]) {
|
||||
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,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// 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 even with 0 data (check division by 0)', async () => {
|
||||
// ACT
|
||||
const byWorkflow = await insightsService.getInsightsByWorkflow({
|
||||
@@ -450,13 +636,18 @@ describe('getInsightsByTime', () => {
|
||||
});
|
||||
|
||||
let project: Project;
|
||||
let otherProject: 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);
|
||||
|
||||
otherProject = await createTeamProject();
|
||||
workflow3 = await createWorkflow({}, otherProject);
|
||||
});
|
||||
|
||||
test('returns empty array when no insights exist', async () => {
|
||||
@@ -625,6 +816,113 @@ describe('getInsightsByTime', () => {
|
||||
failed: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('compacted data are are grouped by time correctly with projectId filter', async () => {
|
||||
// ARRANGE
|
||||
for (const workflow of [workflow1, workflow2, workflow3]) {
|
||||
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',
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// 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', () => {
|
||||
|
||||
@@ -282,7 +282,8 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
|
||||
async getPreviousAndCurrentPeriodTypeAggregates({
|
||||
periodLengthInDays,
|
||||
}: { periodLengthInDays: number }): Promise<
|
||||
projectId,
|
||||
}: { periodLengthInDays: number; projectId?: string }): Promise<
|
||||
Array<{
|
||||
period: 'previous' | 'current';
|
||||
type: 0 | 1 | 2 | 3;
|
||||
@@ -296,7 +297,7 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
${this.getAgeLimitQuery(periodLengthInDays * 2)} AS previous_start
|
||||
`;
|
||||
|
||||
const rawRows = await this.createQueryBuilder('insights')
|
||||
const rawRowsQuery = this.createQueryBuilder('insights')
|
||||
.addCommonTableExpression(cte, 'date_ranges')
|
||||
.select(
|
||||
sql`
|
||||
@@ -312,13 +313,19 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
.addSelect('SUM(value)', 'total_value')
|
||||
// Use a cross join with the CTE
|
||||
.innerJoin('date_ranges', 'date_ranges', '1=1')
|
||||
// Filter to only include data from the last 14 days
|
||||
.where('insights.periodStart >= date_ranges.previous_start')
|
||||
.andWhere('insights.periodStart <= date_ranges.current_end')
|
||||
// Group by both period and type
|
||||
.groupBy('period')
|
||||
.addGroupBy('insights.type')
|
||||
.getRawMany();
|
||||
.addGroupBy('insights.type');
|
||||
|
||||
if (projectId) {
|
||||
rawRowsQuery
|
||||
.innerJoin('insights.metadata', 'metadata')
|
||||
.andWhere('metadata.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
const rawRows = await rawRowsQuery.getRawMany();
|
||||
|
||||
return summaryParser.parse(rawRows);
|
||||
}
|
||||
@@ -333,11 +340,13 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
skip = 0,
|
||||
take = 20,
|
||||
sortBy = 'total:desc',
|
||||
projectId,
|
||||
}: {
|
||||
maxAgeInDays: number;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
sortBy?: string;
|
||||
projectId?: string;
|
||||
}) {
|
||||
const [sortField, sortOrder] = this.parseSortingParams(sortBy);
|
||||
const sumOfExecutions = sql`SUM(CASE WHEN insights.type IN (${TypeToNumber.success.toString()}, ${TypeToNumber.failure.toString()}) THEN value ELSE 0 END)`;
|
||||
@@ -375,6 +384,10 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
.addGroupBy('metadata.projectName')
|
||||
.orderBy(this.escapeField(sortField), sortOrder);
|
||||
|
||||
if (projectId) {
|
||||
rawRowsQuery.andWhere('metadata.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
const count = (await rawRowsQuery.getRawMany()).length;
|
||||
const rawRows = await rawRowsQuery.offset(skip).limit(take).getRawMany();
|
||||
|
||||
@@ -385,14 +398,20 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
maxAgeInDays,
|
||||
periodUnit,
|
||||
insightTypes,
|
||||
}: { maxAgeInDays: number; periodUnit: PeriodUnit; insightTypes: TypeUnit[] }) {
|
||||
projectId,
|
||||
}: {
|
||||
maxAgeInDays: number;
|
||||
periodUnit: PeriodUnit;
|
||||
insightTypes: TypeUnit[];
|
||||
projectId?: string;
|
||||
}) {
|
||||
const cte = sql`SELECT ${this.getAgeLimitQuery(maxAgeInDays)} AS start_date`;
|
||||
|
||||
const typesAggregation = insightTypes.map((type) => {
|
||||
return `SUM(CASE WHEN type = ${TypeToNumber[type]} THEN value ELSE 0 END) AS "${displayTypeName[TypeToNumber[type]]}"`;
|
||||
return `SUM(CASE WHEN insights.type = ${TypeToNumber[type]} THEN value ELSE 0 END) AS "${displayTypeName[TypeToNumber[type]]}"`;
|
||||
});
|
||||
|
||||
const rawRowsQuery = this.createQueryBuilder()
|
||||
const rawRowsQuery = this.createQueryBuilder('insights')
|
||||
.addCommonTableExpression(cte, 'date_range')
|
||||
.select([`${this.getPeriodStartExpr(periodUnit)} as "periodStart"`, ...typesAggregation])
|
||||
.innerJoin('date_range', 'date_range', '1=1')
|
||||
@@ -400,6 +419,12 @@ export class InsightsByPeriodRepository extends Repository<InsightsByPeriod> {
|
||||
.groupBy(this.getPeriodStartExpr(periodUnit))
|
||||
.orderBy(this.getPeriodStartExpr(periodUnit), 'ASC');
|
||||
|
||||
if (projectId) {
|
||||
rawRowsQuery
|
||||
.innerJoin('insights.metadata', 'metadata')
|
||||
.andWhere('metadata.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
const rawRows = await rawRowsQuery.getRawMany();
|
||||
|
||||
return aggregatedInsightsByTimeParser.parse(rawRows);
|
||||
|
||||
@@ -40,11 +40,12 @@ export class InsightsController {
|
||||
async getInsightsSummary(
|
||||
_req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Query payload: InsightsDateFilterDto = { dateRange: 'week' },
|
||||
@Query query: InsightsDateFilterDto = { dateRange: 'week' },
|
||||
): Promise<InsightsSummary> {
|
||||
const dateRangeAndMaxAgeInDays = this.getMaxAgeInDaysAndGranularity(payload);
|
||||
const dateRangeAndMaxAgeInDays = this.getMaxAgeInDaysAndGranularity(query);
|
||||
return await this.insightsService.getInsightsSummary({
|
||||
periodLengthInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||
projectId: query.projectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +65,7 @@ export class InsightsController {
|
||||
skip: payload.skip,
|
||||
take: payload.take,
|
||||
sortBy: payload.sortBy,
|
||||
projectId: payload.projectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,6 +84,7 @@ export class InsightsController {
|
||||
return (await this.insightsService.getInsightsByTime({
|
||||
maxAgeInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||
periodUnit: dateRangeAndMaxAgeInDays.granularity,
|
||||
projectId: payload.projectId,
|
||||
})) as InsightsByTime[];
|
||||
}
|
||||
|
||||
@@ -104,6 +107,7 @@ export class InsightsController {
|
||||
maxAgeInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||
periodUnit: dateRangeAndMaxAgeInDays.granularity,
|
||||
insightTypes: ['time_saved_min'],
|
||||
projectId: payload.projectId,
|
||||
})) as RestrictedInsightsByTime[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,11 @@ export class InsightsService {
|
||||
|
||||
async getInsightsSummary({
|
||||
periodLengthInDays,
|
||||
}: { periodLengthInDays: number }): Promise<InsightsSummary> {
|
||||
projectId,
|
||||
}: { periodLengthInDays: number; projectId?: string }): Promise<InsightsSummary> {
|
||||
const rows = await this.insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates({
|
||||
periodLengthInDays,
|
||||
projectId,
|
||||
});
|
||||
|
||||
// Initialize data structures for both periods
|
||||
@@ -151,17 +153,20 @@ export class InsightsService {
|
||||
skip = 0,
|
||||
take = 10,
|
||||
sortBy = 'total:desc',
|
||||
projectId,
|
||||
}: {
|
||||
maxAgeInDays: number;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
sortBy?: string;
|
||||
projectId?: string;
|
||||
}) {
|
||||
const { count, rows } = await this.insightsByPeriodRepository.getInsightsByWorkflow({
|
||||
maxAgeInDays,
|
||||
skip,
|
||||
take,
|
||||
sortBy,
|
||||
projectId,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -175,11 +180,18 @@ export class InsightsService {
|
||||
periodUnit,
|
||||
// Default to all insight types
|
||||
insightTypes = Object.keys(TypeToNumber) as TypeUnit[],
|
||||
}: { maxAgeInDays: number; periodUnit: PeriodUnit; insightTypes?: TypeUnit[] }) {
|
||||
projectId,
|
||||
}: {
|
||||
maxAgeInDays: number;
|
||||
periodUnit: PeriodUnit;
|
||||
insightTypes?: TypeUnit[];
|
||||
projectId?: string;
|
||||
}) {
|
||||
const rows = await this.insightsByPeriodRepository.getInsightsByTime({
|
||||
maxAgeInDays,
|
||||
periodUnit,
|
||||
insightTypes,
|
||||
projectId,
|
||||
});
|
||||
|
||||
return rows.map((r) => {
|
||||
|
||||
Reference in New Issue
Block a user