Skip to content

Commit ea739d0

Browse files
Copilotkarpikpl
andauthored
Add date range filtering support for GitHub Copilot Metrics API beyond 28 days (#220)
* Initial plan * Add date range selector and dynamic date filtering support Co-authored-by: karpikpl <[email protected]> * Fix API pagination and complete date range functionality testing Co-authored-by: karpikpl <[email protected]> * Fix reactive ref access in MainComponent.vue Co-authored-by: karpikpl <[email protected]> * Fix UI alignment and reactive ref access issues Co-authored-by: karpikpl <[email protected]> * fix mocks and padding * Add date range filtering to languages/editors tabs and hide date selector for seats tab Co-authored-by: karpikpl <[email protected]> * Fix breakdown component reactivity to update with date range changes Co-authored-by: karpikpl <[email protected]> * Fix date range reset when switching to seat analysis tab Co-authored-by: karpikpl <[email protected]> * Fix seats tab display issue - show seats data regardless of metrics state Co-authored-by: karpikpl <[email protected]> * back to server fetch * hide filter when signInRequired --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: karpikpl <[email protected]> Co-authored-by: Piotr Karpala <[email protected]>
1 parent 3055772 commit ea739d0

File tree

6 files changed

+449
-152
lines changed

6 files changed

+449
-152
lines changed

app/components/BreakdownComponent.vue

Lines changed: 78 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<div class="spacing-25"/>
99
<div class="text-h6 mb-1">Number of {{ breakdownDisplayNamePlural }}</div>
1010
<div class="text-caption">
11-
Over the last 28 days
11+
{{ dateRangeDescription }}
1212
</div>
1313
<p class="text-h4">{{ numberOfBreakdowns }}</p>
1414
</div>
@@ -79,7 +79,7 @@
7979
</template>
8080

8181
<script lang="ts">
82-
import { defineComponent, ref, toRef } from 'vue';
82+
import { defineComponent, ref, toRef, watch } from 'vue';
8383
import type { Metrics } from '@/model/Metrics';
8484
import { Breakdown } from '@/model/Breakdown';
8585
import { Pie } from 'vue-chartjs'
@@ -122,6 +122,10 @@ export default defineComponent({
122122
breakdownKey: {
123123
type: String,
124124
required: true
125+
},
126+
dateRangeDescription: {
127+
type: String,
128+
default: 'Over the last 28 days'
125129
}
126130
},
127131
setup(props) {
@@ -157,76 +161,87 @@ export default defineComponent({
157161
'#7CFC00' // Lawn Green
158162
]);
159163
160-
const data = toRef(props, 'metrics').value;
164+
// Function to process breakdown data
165+
const processBreakdownData = (data: Metrics[]) => {
166+
// Reset the breakdown list
167+
breakdownList.value = [];
168+
169+
// Process the breakdown separately
170+
data.forEach((m: Metrics) => m.breakdown.forEach(breakdownData =>
171+
{
172+
const breakdownName = breakdownData[props.breakdownKey as keyof typeof breakdownData] as string;
173+
let breakdown = breakdownList.value.find(b => b.name === breakdownName);
161174
162-
// Process the breakdown separately
163-
data.forEach((m: Metrics) => m.breakdown.forEach(breakdownData =>
164-
{
165-
const breakdownName = breakdownData[props.breakdownKey as keyof typeof breakdownData] as string;
166-
let breakdown = breakdownList.value.find(b => b.name === breakdownName);
175+
if (!breakdown) {
176+
// Create a new breakdown object if it does not exist
177+
breakdown = new Breakdown({
178+
name: breakdownName,
179+
acceptedPrompts: breakdownData.acceptances_count,
180+
suggestedPrompts: breakdownData.suggestions_count,
181+
suggestedLinesOfCode: breakdownData.lines_suggested,
182+
acceptedLinesOfCode: breakdownData.lines_accepted,
183+
});
184+
breakdownList.value.push(breakdown);
185+
} else {
186+
// Update the existing breakdown object
187+
breakdown.acceptedPrompts += breakdownData.acceptances_count;
188+
breakdown.suggestedPrompts += breakdownData.suggestions_count;
189+
breakdown.suggestedLinesOfCode += breakdownData.lines_suggested;
190+
breakdown.acceptedLinesOfCode += breakdownData.lines_accepted;
191+
}
192+
// Recalculate the acceptance rates
193+
breakdown.acceptanceRateByCount = breakdown.suggestedPrompts !== 0 ? (breakdown.acceptedPrompts / breakdown.suggestedPrompts) * 100 : 0;
194+
breakdown.acceptanceRateByLines = breakdown.suggestedLinesOfCode !== 0 ? (breakdown.acceptedLinesOfCode / breakdown.suggestedLinesOfCode) * 100 : 0;
167195
168-
if (!breakdown) {
169-
// Create a new breakdown object if it does not exist
170-
breakdown = new Breakdown({
171-
name: breakdownName,
172-
acceptedPrompts: breakdownData.acceptances_count,
173-
suggestedPrompts: breakdownData.suggestions_count,
174-
suggestedLinesOfCode: breakdownData.lines_suggested,
175-
acceptedLinesOfCode: breakdownData.lines_accepted,
176-
});
177-
breakdownList.value.push(breakdown);
178-
} else {
179-
// Update the existing breakdown object
180-
breakdown.acceptedPrompts += breakdownData.acceptances_count;
181-
breakdown.suggestedPrompts += breakdownData.suggestions_count;
182-
breakdown.suggestedLinesOfCode += breakdownData.lines_suggested;
183-
breakdown.acceptedLinesOfCode += breakdownData.lines_accepted;
184-
}
185-
// Recalculate the acceptance rates
186-
breakdown.acceptanceRateByCount = breakdown.suggestedPrompts !== 0 ? (breakdown.acceptedPrompts / breakdown.suggestedPrompts) * 100 : 0;
187-
breakdown.acceptanceRateByLines = breakdown.suggestedLinesOfCode !== 0 ? (breakdown.acceptedLinesOfCode / breakdown.suggestedLinesOfCode) * 100 : 0;
196+
// Log each breakdown for debugging
197+
// console.log('Breakdown:', breakdown);
198+
}));
188199
189-
// Log each breakdown for debugging
190-
// console.log('Breakdown:', breakdown);
191-
}));
200+
//Sort breakdowns map by accepted prompts
201+
breakdownList.value.sort((a, b) => b.acceptedPrompts - a.acceptedPrompts);
192202
193-
//Sort breakdowns map by accepted prompts
194-
breakdownList.value.sort((a, b) => b.acceptedPrompts - a.acceptedPrompts);
203+
// Get the top 5 breakdowns by accepted prompts
204+
const top5BreakdownsAcceptedPrompts = breakdownList.value.slice(0, 5);
205+
206+
breakdownsChartDataTop5AcceptedPrompts.value = {
207+
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
208+
datasets: [
209+
{
210+
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptedPrompts),
211+
backgroundColor: pieChartColors.value,
212+
},
213+
],
214+
};
195215
196-
// Get the top 5 breakdowns by accepted prompts
197-
const top5BreakdownsAcceptedPrompts = breakdownList.value.slice(0, 5);
198-
199-
breakdownsChartDataTop5AcceptedPrompts.value = {
200-
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
201-
datasets: [
202-
{
203-
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptedPrompts),
204-
backgroundColor: pieChartColors.value,
205-
},
206-
],
207-
};
216+
breakdownsChartDataTop5AcceptedPromptsByLines.value = {
217+
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
218+
datasets: [
219+
{
220+
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptanceRateByLines.toFixed(2)),
221+
backgroundColor: pieChartColors.value,
222+
},
223+
],
224+
};
208225
209-
breakdownsChartDataTop5AcceptedPromptsByLines.value = {
210-
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
211-
datasets: [
212-
{
213-
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptanceRateByLines.toFixed(2)),
214-
backgroundColor: pieChartColors.value,
215-
},
216-
],
217-
};
226+
breakdownsChartDataTop5AcceptedPromptsByCounts.value = {
227+
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
228+
datasets: [
229+
{
230+
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptanceRateByCount.toFixed(2)),
231+
backgroundColor: pieChartColors.value,
232+
},
233+
],
234+
};
218235
219-
breakdownsChartDataTop5AcceptedPromptsByCounts.value = {
220-
labels: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.name),
221-
datasets: [
222-
{
223-
data: top5BreakdownsAcceptedPrompts.map(breakdown => breakdown.acceptanceRateByCount.toFixed(2)),
224-
backgroundColor: pieChartColors.value,
225-
},
226-
],
236+
numberOfBreakdowns.value = breakdownList.value.length;
227237
};
228238
229-
numberOfBreakdowns.value = breakdownList.value.length;
239+
// Watch for changes in metrics prop and re-process data
240+
watch(() => props.metrics, (newMetrics) => {
241+
if (newMetrics && Array.isArray(newMetrics)) {
242+
processBreakdownData(newMetrics);
243+
}
244+
}, { immediate: true, deep: true });
230245
231246
return { chartOptions, breakdownList, numberOfBreakdowns,
232247
breakdownsChartData, breakdownsChartDataTop5AcceptedPrompts, breakdownsChartDataTop5AcceptedPromptsByLines, breakdownsChartDataTop5AcceptedPromptsByCounts };

app/components/CopilotChatViewer.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<span class="text-caption" style="font-size: 10px !important;">This metric represents the total number of turns (interactions) with the Copilot over the past 28 days. A 'turn' includes both user inputs and Copilot's responses.</span>
1313
</v-card>
1414
</v-tooltip>
15-
<div class="text-caption">Over the last 28 days</div>
15+
<div class="text-caption">{{ dateRangeDescription }}</div>
1616
<p class="text-h4">{{ cumulativeNumberTurns }}</p>
1717
</div>
1818
</v-card-item>
@@ -30,7 +30,7 @@
3030
<span class="text-caption" style="font-size: 10px !important;">This metric shows the total number of lines of code suggested by Copilot that have been accepted by users over the past 28 days.</span>
3131
</v-card>
3232
</v-tooltip>
33-
<div class="text-caption">Over the last 28 days</div>
33+
<div class="text-caption">{{ dateRangeDescription }}</div>
3434
<p class="text-h4">{{ cumulativeNumberAcceptances }}</p>
3535
</div>
3636
</v-card-item>
@@ -103,6 +103,10 @@ props: {
103103
metrics: {
104104
type: Object,
105105
required: true
106+
},
107+
dateRangeDescription: {
108+
type: String,
109+
default: 'Over the last 28 days'
106110
}
107111
},
108112
setup(props) {

app/components/DateRangeSelector.vue

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<template>
2+
<v-card class="pa-4 ma-4" elevation="2">
3+
<v-card-title class="text-h6 pb-2">Date Range Filter</v-card-title>
4+
<v-row align="end">
5+
<v-col cols="12" sm="4">
6+
<v-text-field
7+
v-model="fromDate"
8+
label="From Date"
9+
type="date"
10+
variant="outlined"
11+
density="compact"
12+
@update:model-value="updateDateRange"
13+
/>
14+
</v-col>
15+
<v-col cols="12" sm="4">
16+
<v-text-field
17+
v-model="toDate"
18+
label="To Date"
19+
type="date"
20+
variant="outlined"
21+
density="compact"
22+
@update:model-value="updateDateRange"
23+
/>
24+
</v-col>
25+
<v-col cols="12" sm="4" class="d-flex align-center justify-start" style="padding-bottom: 35px;">
26+
<v-btn
27+
color="primary"
28+
variant="outlined"
29+
size="default"
30+
class="mr-3"
31+
@click="resetToDefault"
32+
>
33+
Last 28 Days
34+
</v-btn>
35+
<v-btn
36+
color="primary"
37+
size="default"
38+
:loading="loading"
39+
@click="applyDateRange"
40+
>
41+
Apply
42+
</v-btn>
43+
</v-col>
44+
</v-row>
45+
<v-card-text class="pt-2">
46+
<span class="text-caption text-medium-emphasis">
47+
{{ dateRangeText }}
48+
</span>
49+
</v-card-text>
50+
</v-card>
51+
</template>
52+
53+
<script setup lang="ts">
54+
interface Props {
55+
loading?: boolean
56+
}
57+
58+
interface Emits {
59+
(e: 'date-range-changed', value: { since?: string; until?: string; description: string }): void
60+
}
61+
62+
withDefaults(defineProps<Props>(), {
63+
loading: false
64+
})
65+
66+
const emit = defineEmits<Emits>()
67+
68+
// Calculate default dates (last 28 days)
69+
const today = new Date()
70+
const defaultFromDate = new Date(today.getTime() - 27 * 24 * 60 * 60 * 1000) // 27 days ago to include today
71+
72+
const fromDate = ref(formatDate(defaultFromDate))
73+
const toDate = ref(formatDate(today))
74+
75+
function formatDate(date: Date): string {
76+
return date.toISOString().split('T')[0] || ''
77+
}
78+
79+
function parseDate(dateString: string): Date {
80+
return new Date(dateString + 'T00:00:00.000Z')
81+
}
82+
83+
const dateRangeText = computed(() => {
84+
if (!fromDate.value || !toDate.value) {
85+
return 'Select date range'
86+
}
87+
88+
const from = parseDate(fromDate.value)
89+
const to = parseDate(toDate.value)
90+
const diffDays = Math.ceil((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24)) + 1
91+
92+
if (diffDays === 1) {
93+
return `For ${from.toLocaleDateString()}`
94+
} else if (diffDays <= 28 && isLast28Days()) {
95+
return 'Over the last 28 days'
96+
} else {
97+
return `From ${from.toLocaleDateString()} to ${to.toLocaleDateString()} (${diffDays} days)`
98+
}
99+
})
100+
101+
function isLast28Days(): boolean {
102+
if (!fromDate.value || !toDate.value) return false
103+
104+
const today = new Date()
105+
const expectedFromDate = new Date(today.getTime() - 27 * 24 * 60 * 60 * 1000)
106+
107+
const from = parseDate(fromDate.value)
108+
const to = parseDate(toDate.value)
109+
110+
return (
111+
from.toDateString() === expectedFromDate.toDateString() &&
112+
to.toDateString() === today.toDateString()
113+
)
114+
}
115+
116+
function updateDateRange() {
117+
// This function is called when dates change, but we don't auto-apply
118+
// User needs to click Apply button
119+
}
120+
121+
function resetToDefault() {
122+
const today = new Date()
123+
const defaultFrom = new Date(today.getTime() - 27 * 24 * 60 * 60 * 1000)
124+
125+
fromDate.value = formatDate(defaultFrom)
126+
toDate.value = formatDate(today)
127+
}
128+
129+
function applyDateRange() {
130+
if (!fromDate.value || !toDate.value) {
131+
return
132+
}
133+
134+
const from = parseDate(fromDate.value)
135+
const to = parseDate(toDate.value)
136+
137+
if (from > to) {
138+
// Swap dates if from is after to
139+
const temp = fromDate.value
140+
fromDate.value = toDate.value
141+
toDate.value = temp
142+
}
143+
144+
// Emit the date range change
145+
emit('date-range-changed', {
146+
since: fromDate.value,
147+
until: toDate.value,
148+
description: dateRangeText.value
149+
})
150+
}
151+
152+
// Initialize with default range on mount
153+
onMounted(() => {
154+
applyDateRange()
155+
})
156+
</script>

0 commit comments

Comments
 (0)