Skip to content

Commit 1efa5d8

Browse files
committed
1) update GitHub API to support new /metrics route, replace the old /usage route 2) add MetricsToUsageConverter class, to map the new returned metrics schedma to old metrics schema, so the caller don't need to update more. 3) add CopilotUsageChecker to check the data quality of the new fetched schema, and display it in api-response page.
1 parent 0dd5f1b commit 1efa5d8

9 files changed

+5336
-134
lines changed

src/api/GitHubApi.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
//Make a call to the GitHub API to get Copilot Metrics, the API is /api/github/orgs/toussaintt/copilot/usage
2-
//Add the header Accept: application/vnd.github+json to the request
3-
//Add also the Authorization: Bearer <token> header where <token> is hardcoded for now
4-
//Also add X-GitHub-Api-Version: 2022-11-28 header
5-
//Return the response from the API
6-
71
import axios from "axios";
8-
92
import { Metrics } from "../model/Metrics";
10-
import organizationMockedResponse from '../assets/organization_response_sample.json';
11-
import enterpriseMockedResponse from '../assets/enterprise_response_sample.json';
3+
import { CopilotMetrics } from '../model/Copilot_Metrics';
4+
import { convertToMetrics } from './MetricsToUsageConverter';
5+
import organizationMockedResponse from '../assets/organization_usage_response_sample.json';
6+
import enterpriseMockedResponse from '../assets/enterprise_usage_response_sample.json';
7+
import organizationMockedMetricsResponse from '../assets/organization_metrics_response_sample.json';
8+
import enterpriseMockedMetricsResponse from '../assets/enterprise_metrics_response_sample.json';
129
import config from '../config';
1310

1411
const headers = {
@@ -17,27 +14,43 @@ const headers = {
1714
...(config.github.token ? { Authorization: `token ${config.github.token}` } : {})
1815
};
1916

20-
export const getMetricsApi = async (): Promise<Metrics[]> => {
17+
const ensureCopilotMetrics = (data: any[]): CopilotMetrics[] => {
18+
return data.map(item => {
19+
if (!item.copilot_ide_code_completions) {
20+
item.copilot_ide_code_completions = { editors: [] };
21+
}
22+
item.copilot_ide_code_completions.editors?.forEach((editor: any) => {
23+
editor.models?.forEach((model: any) => {
24+
if (!model.languages) {
25+
model.languages = [];
26+
}
27+
});
28+
});
29+
return item as CopilotMetrics;
30+
});
31+
};
2132

33+
export const getMetricsApi = async (): Promise<{ metrics: Metrics[], original: CopilotMetrics[] }> => {
2234
let response;
23-
let metricsData;
35+
let metricsData: Metrics[];
36+
let originalData: CopilotMetrics[];
2437

2538
if (config.mockedData) {
2639
console.log("Using mock data. Check VUE_APP_MOCKED_DATA variable.");
27-
response = config.scope.type === "organization" ? organizationMockedResponse : enterpriseMockedResponse;
28-
metricsData = response.map((item: any) => new Metrics(item));
40+
response = config.scope.type === "organization" ? organizationMockedMetricsResponse : enterpriseMockedMetricsResponse;
41+
originalData = ensureCopilotMetrics(response);
42+
metricsData = convertToMetrics(originalData);
2943
} else {
3044
response = await axios.get(
31-
`${config.github.apiUrl}/copilot/usage`,
45+
`${config.github.apiUrl}/copilot/metrics`,
3246
{
33-
headers
47+
headers
3448
}
3549
);
36-
37-
38-
metricsData = response.data.map((item: any) => new Metrics(item));
50+
originalData = ensureCopilotMetrics(response.data);
51+
metricsData = convertToMetrics(originalData);
3952
}
40-
return metricsData;
53+
return { metrics: metricsData, original: originalData };
4154
};
4255

4356
export const getTeams = async (): Promise<string[]> => {
@@ -48,12 +61,12 @@ export const getTeams = async (): Promise<string[]> => {
4861
return response.data;
4962
}
5063

51-
export const getTeamMetricsApi = async (): Promise<Metrics[]> => {
64+
export const getTeamMetricsApi = async (): Promise<{ metrics: Metrics[], original: CopilotMetrics[] }> => {
5265
console.log("config.github.team: " + config.github.team);
5366

5467
if (config.github.team && config.github.team.trim() !== '') {
5568
const response = await axios.get(
56-
`${config.github.apiUrl}/team/${config.github.team}/copilot/usage`,
69+
`${config.github.apiUrl}/team/${config.github.team}/copilot/metrics`,
5770
{
5871
headers: {
5972
Accept: "application/vnd.github+json",
@@ -63,9 +76,10 @@ export const getTeamMetricsApi = async (): Promise<Metrics[]> => {
6376
}
6477
);
6578

66-
return response.data.map((item: any) => new Metrics(item));
79+
const originalData = ensureCopilotMetrics(response.data);
80+
const metricsData = convertToMetrics(originalData);
81+
return { metrics: metricsData, original: originalData };
6782
}
6883

69-
return [];
70-
84+
return { metrics: [], original: [] };
7185
}

src/api/MetricsToUsageConverter.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Metrics, BreakdownData } from "../model/Metrics";
2+
import { CopilotMetrics } from '../model/Copilot_Metrics';
3+
4+
export const convertToMetrics = (copilotMetrics: CopilotMetrics[]): Metrics[] => {
5+
try {
6+
const usageData: Metrics[] = copilotMetrics.map(metric => {
7+
const breakdown: BreakdownData[] = [];
8+
9+
metric.copilot_ide_code_completions?.editors?.forEach(editor => {
10+
editor.models?.forEach(model => {
11+
model.languages?.forEach(language => {
12+
breakdown.push(new BreakdownData({
13+
language: language.name,
14+
editor: editor.name,
15+
suggestions_count: language.total_code_suggestions,
16+
acceptances_count: language.total_code_acceptances,
17+
lines_suggested: language.total_code_lines_suggested,
18+
lines_accepted: language.total_code_lines_accepted,
19+
active_users: language.total_engaged_users
20+
}));
21+
});
22+
});
23+
});
24+
25+
const totalChatInsertions = metric.copilot_ide_chat?.editors?.reduce((sum, editor) =>
26+
sum + editor.models?.reduce((sum, model) => sum + model.total_chat_insertion_events, 0), 0) || 0;
27+
28+
const totalChatCopies = metric.copilot_ide_chat?.editors?.reduce((sum, editor) =>
29+
sum + editor.models?.reduce((sum, model) => sum + model.total_chat_copy_events, 0), 0) || 0;
30+
31+
console.log(`Date: ${metric.date}`);
32+
console.log(`Total Chat Insertions: ${totalChatInsertions}`);
33+
console.log(`Total Chat Copies: ${totalChatCopies}`);
34+
35+
return new Metrics({
36+
day: metric.date,
37+
total_suggestions_count: breakdown.reduce((sum, item) => sum + item.suggestions_count, 0),
38+
total_acceptances_count: breakdown.reduce((sum, item) => sum + item.acceptances_count, 0),
39+
total_lines_suggested: breakdown.reduce((sum, item) => sum + item.lines_suggested, 0),
40+
total_lines_accepted: breakdown.reduce((sum, item) => sum + item.lines_accepted, 0),
41+
total_active_users: metric.total_active_users || 0,
42+
total_chat_acceptances: totalChatInsertions + totalChatCopies,
43+
total_chat_turns: metric.copilot_ide_chat?.editors?.reduce((sum, editor) =>
44+
sum + editor.models?.reduce((sum, model) => sum + model.total_chats, 0), 0) || 0,
45+
total_active_chat_users: metric.copilot_ide_chat?.total_engaged_users || 0,
46+
breakdown: breakdown
47+
});
48+
});
49+
50+
return usageData;
51+
} catch (error) {
52+
console.error('Error converting metrics to usage format:', error);
53+
return [];
54+
}
55+
};

src/api/MetricsValidator.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { CopilotMetrics } from '../model/Copilot_Metrics';
2+
3+
export class MetricsValidator {
4+
metrics: CopilotMetrics[];
5+
6+
constructor(metrics: CopilotMetrics[]) {
7+
this.metrics = metrics;
8+
}
9+
10+
checkContinuousDates(): string[] {
11+
const dates = this.metrics.map(metric => new Date(metric.date));
12+
dates.sort((a, b) => a.getTime() - b.getTime());
13+
14+
const missingDates: string[] = [];
15+
for (let i = 1; i < dates.length; i++) {
16+
const prevDate = dates[i - 1];
17+
const currDate = dates[i];
18+
const diffDays = (currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24);
19+
20+
if (diffDays > 1) {
21+
for (let d = 1; d < diffDays; d++) {
22+
const missingDate = new Date(prevDate.getTime() + d * (1000 * 60 * 60 * 24));
23+
missingDates.push(missingDate.toISOString().split('T')[0]);
24+
}
25+
}
26+
}
27+
return missingDates;
28+
}
29+
30+
validateCodeCompletions(): { date: string, editor: string, language: string }[] {
31+
const invalidEntries: { date: string, editor: string, language: string }[] = [];
32+
33+
this.metrics.forEach(metric => {
34+
if (metric.copilot_ide_code_completions?.editors) {
35+
metric.copilot_ide_code_completions.editors.forEach(editor => {
36+
if (editor.models) {
37+
editor.models.forEach(model => {
38+
if (model.languages) {
39+
model.languages.forEach(language => {
40+
if (language.total_code_acceptances > language.total_code_suggestions ||
41+
language.total_code_lines_accepted > language.total_code_lines_suggested) {
42+
invalidEntries.push({
43+
date: metric.date,
44+
editor: editor.name,
45+
language: language.name
46+
});
47+
}
48+
});
49+
}
50+
});
51+
}
52+
});
53+
}
54+
});
55+
56+
return invalidEntries;
57+
}
58+
59+
validateChatEngagedUsers(): { date: string, editor: string, total_engaged_users: number }[] {
60+
const invalidEntries: { date: string, editor: string, total_engaged_users: number }[] = [];
61+
62+
this.metrics.forEach(metric => {
63+
if (metric.copilot_ide_chat?.editors) {
64+
metric.copilot_ide_chat.editors.forEach(editor => {
65+
if (editor.models) {
66+
const totalModelEngagedUsers = editor.models.reduce((sum, model) =>
67+
sum + (model.total_engaged_users || 0), 0);
68+
if (totalModelEngagedUsers < (editor.total_engaged_users || 0)) {
69+
invalidEntries.push({
70+
date: metric.date,
71+
editor: editor.name,
72+
total_engaged_users: editor.total_engaged_users
73+
});
74+
}
75+
}
76+
});
77+
}
78+
});
79+
80+
return invalidEntries;
81+
}
82+
83+
checkAllMetrics() {
84+
const nonContinuousDates = this.checkContinuousDates();
85+
const invalidCodeCompletions = this.validateCodeCompletions();
86+
const invalidChatEngagedUsers = this.validateChatEngagedUsers();
87+
88+
return {
89+
nonContinuousDates,
90+
invalidCodeCompletions,
91+
invalidChatEngagedUsers
92+
};
93+
}
94+
}

0 commit comments

Comments
 (0)