Skip to content

Commit dd1426d

Browse files
feat: add sorting functions for metrics and report day totals, update data processing to ensure chronological order (#330)
1 parent e783855 commit dd1426d

4 files changed

Lines changed: 51 additions & 12 deletions

File tree

server/api/metrics.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { convertToMetrics } from '@/model/MetricsToUsageConverter';
22
import type { MetricsApiResponse } from "@/types/metricsApiResponse";
33
import { getMetricsDataV2 } from '../../shared/utils/metrics-util-v2';
44

5+
function sortMetricsByDay<T extends { day: string }>(metrics: T[]): T[] {
6+
return [...metrics].sort((left, right) => left.day.localeCompare(right.day));
7+
}
8+
59
export default defineEventHandler(async (event) => {
610

711
const logger = console;
@@ -11,7 +15,7 @@ export default defineEventHandler(async (event) => {
1115
const { metrics: usageData, reportData } = await getMetricsDataV2(event);
1216

1317
// metrics is the old API format
14-
const metricsData = convertToMetrics(usageData);
18+
const metricsData = sortMetricsByDay(convertToMetrics(usageData));
1519

1620
const result = { metrics: metricsData, usage: usageData, reportData } as MetricsApiResponse;
1721
return result;

server/services/report-transformer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ const CHAT_FEATURES = [
2121
'chat_panel_custom_mode', 'chat_panel_unknown_mode', 'chat_inline',
2222
];
2323

24+
export function sortReportDayTotalsByDay(dayTotals: ReportDayTotals[]): ReportDayTotals[] {
25+
return [...dayTotals].sort((left, right) => left.day.localeCompare(right.day));
26+
}
27+
28+
export function sortCopilotMetricsByDate(metrics: CopilotMetrics[]): CopilotMetrics[] {
29+
return [...metrics].sort((left, right) => left.date.localeCompare(right.date));
30+
}
31+
2432
/**
2533
* Transform an entire OrgReport into an array of CopilotMetrics records.
2634
*/
2735
export function transformReportToMetrics(report: OrgReport): CopilotMetrics[] {
28-
return report.day_totals.map(transformDayToMetrics);
36+
return sortReportDayTotalsByDay(report.day_totals).map(transformDayToMetrics);
2937
}
3038

3139
/**

shared/utils/metrics-util-v2.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,25 @@ import { filterHolidaysFromMetrics } from '@/utils/dateUtils';
1818
import { getMetricsData as getLegacyMetricsData } from './metrics-util';
1919
import { getMetricsByDateRange, getReportDataByDateRange, saveMetrics } from '../../server/storage/metrics-storage';
2020
import { fetchLatestReport, type MetricsReportRequest } from '../../server/services/github-copilot-usage-api';
21-
import { transformReportToMetrics } from '../../server/services/report-transformer';
21+
import {
22+
sortCopilotMetricsByDate,
23+
sortReportDayTotalsByDay,
24+
transformReportToMetrics,
25+
} from '../../server/services/report-transformer';
2226
import { isMockMode } from '../../server/services/github-copilot-usage-api-mock';
2327

2428
export interface MetricsDataResult {
2529
metrics: CopilotMetrics[];
2630
reportData: ReportDayTotals[];
2731
}
2832

33+
function sortMetricsDataResult(result: MetricsDataResult): MetricsDataResult {
34+
return {
35+
metrics: sortCopilotMetricsByDate(result.metrics),
36+
reportData: sortReportDayTotalsByDay(result.reportData),
37+
};
38+
}
39+
2940
/**
3041
* Returns true ONLY when USE_LEGACY_API is explicitly set to "true".
3142
* Default behavior is new API — no legacy calls unless opted in.
@@ -77,7 +88,7 @@ async function fetchFromNewApi(
7788
});
7889
}
7990

80-
return { metrics, reportData };
91+
return sortMetricsDataResult({ metrics, reportData });
8192
}
8293

8394
/**
@@ -106,15 +117,15 @@ export async function getMetricsDataV2(event: H3Event<EventHandlerRequest>): Pro
106117
if (isLegacyMode()) {
107118
logger.info('Using mocked data mode (legacy format — USE_LEGACY_API=true)');
108119
const metrics = await getLegacyMetricsData(event);
109-
return { metrics, reportData: [] };
120+
return sortMetricsDataResult({ metrics, reportData: [] });
110121
}
111122
// Default: exercise full new-API mock pipeline
112123
logger.info('Using mocked data mode (new API format via HTTP download)');
113124
const identifier = options.githubOrg || options.githubEnt || 'mock-org';
114125
const scope = (options.scope || 'organization') as MetricsReportRequest['scope'];
115126
const report = await fetchLatestReport({ scope, identifier }, new Headers());
116127
const metrics = transformReportToMetrics(report);
117-
return { metrics, reportData: report.day_totals };
128+
return sortMetricsDataResult({ metrics, reportData: report.day_totals });
118129
}
119130

120131
const identifier = options.githubOrg || options.githubEnt || '';
@@ -146,7 +157,7 @@ export async function getMetricsDataV2(event: H3Event<EventHandlerRequest>): Pro
146157
options.excludeHolidays || false,
147158
options.locale
148159
);
149-
return { metrics: filteredMetrics, reportData };
160+
return sortMetricsDataResult({ metrics: filteredMetrics, reportData });
150161
}
151162

152163
// DB empty — sync on miss: fetch from API, store, return
@@ -165,7 +176,7 @@ export async function getMetricsDataV2(event: H3Event<EventHandlerRequest>): Pro
165176
options.excludeHolidays || false,
166177
options.locale
167178
);
168-
return { metrics: filteredMetrics, reportData: result.reportData };
179+
return sortMetricsDataResult({ metrics: filteredMetrics, reportData: result.reportData });
169180
} catch (error) {
170181
// If it's already an H3 error (like 401), re-throw
171182
if (error && typeof error === 'object' && 'statusCode' in error) throw error;
@@ -189,15 +200,18 @@ export async function getMetricsDataV2(event: H3Event<EventHandlerRequest>): Pro
189200
if (isLegacyMode()) {
190201
logger.info('Using legacy Copilot Metrics API (USE_LEGACY_API=true, direct, no DB)');
191202
const metrics = await getLegacyMetricsData(event);
192-
return { metrics: filterHolidaysFromMetrics(metrics, options.excludeHolidays || false, options.locale), reportData: [] };
203+
return sortMetricsDataResult({
204+
metrics: filterHolidaysFromMetrics(metrics, options.excludeHolidays || false, options.locale),
205+
reportData: []
206+
});
193207
}
194208

195209
logger.info('Using new Copilot Metrics API (direct, no DB)');
196210
const result = await fetchFromNewApi(options, event.context.headers);
197-
return {
211+
return sortMetricsDataResult({
198212
metrics: filterHolidaysFromMetrics(result.metrics, options.excludeHolidays || false, options.locale),
199213
reportData: result.reportData
200-
};
214+
});
201215
}
202216

203217
/**
@@ -254,5 +268,5 @@ async function fetchAndStore(
254268
});
255269
}
256270

257-
return { metrics, reportData };
271+
return sortMetricsDataResult({ metrics, reportData });
258272
}

tests/api-migration-integration.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ describe('API Migration Integration', () => {
3030
expect(completions?.languages?.length).toBeGreaterThan(0);
3131
});
3232

33+
it('should sort transformed metrics chronologically when report day_totals are out of order', () => {
34+
const report = generateMockReport('2026-02-20', '2026-02-22');
35+
report.day_totals = [report.day_totals[2], report.day_totals[0], report.day_totals[1]];
36+
37+
const metrics = transformReportToMetrics(report);
38+
39+
expect(metrics.map(metric => metric.date)).toEqual([
40+
'2026-02-20',
41+
'2026-02-21',
42+
'2026-02-22'
43+
]);
44+
});
45+
3346
it('should handle NDJSON parsing (backward compat)', () => {
3447
const ndjson = '{"date":"2026-02-20","total_active_users":100}\n{"date":"2026-02-21","total_active_users":110}';
3548
const parsed = parseNDJSON(ndjson);

0 commit comments

Comments
 (0)