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:
Irénée
2025-09-17 09:47:08 +01:00
committed by GitHub
parent 0173d8f707
commit 8086a21eb2
9 changed files with 697 additions and 39 deletions

View File

@@ -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',

View File

@@ -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', () => {

View File

@@ -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);

View File

@@ -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[];
}
}

View File

@@ -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) => {