From 0aa478e6e287bf940f2a06667746ca9c187ff1cf Mon Sep 17 00:00:00 2001 From: Piotr Karpala Date: Mon, 13 Jul 2026 21:38:07 -0400 Subject: [PATCH] fix(metrics): skip synthetic 'others'/'Unknown' buckets when picking top chat model (#420) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bbf27d66-9aca-451b-85ee-3a32e0775d52 --- app/components/MetricsViewer.vue | 3 +- app/components/MyUsageViewer.vue | 6 +- tests/top-chat-model.nuxt.spec.ts | 130 ++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/top-chat-model.nuxt.spec.ts diff --git a/app/components/MetricsViewer.vue b/app/components/MetricsViewer.vue index 0ffe900c..6a877174 100644 --- a/app/components/MetricsViewer.vue +++ b/app/components/MetricsViewer.vue @@ -522,7 +522,8 @@ export default defineComponent({ for (const day of data) { for (const mf of (day.totals_by_model_feature ?? [])) { if (mf.feature === 'code_completion') continue; - const key = mf.model ?? 'Unknown'; + if (!mf.model || ['others', 'unknown'].includes(mf.model.toLowerCase())) continue; + const key = mf.model; modelTotals[key] = (modelTotals[key] ?? 0) + (mf.user_initiated_interaction_count ?? 0); } } diff --git a/app/components/MyUsageViewer.vue b/app/components/MyUsageViewer.vue index 525fdc0a..63e28897 100644 --- a/app/components/MyUsageViewer.vue +++ b/app/components/MyUsageViewer.vue @@ -541,9 +541,11 @@ export default defineComponent({ const topModel = computed(() => { const models = data.value?.totals?.totals_by_model_feature; if (!models || models.length === 0) return null; - return [...models].sort((a, b) => + return [...models].filter(mf => + mf.model && !['others', 'unknown'].includes(mf.model.toLowerCase()) + ).sort((a, b) => b.user_initiated_interaction_count - a.user_initiated_interaction_count - )[0]; + )[0] ?? null; }); // Per-day ai_credits_used chart — only renders when GitHub returned the field diff --git a/tests/top-chat-model.nuxt.spec.ts b/tests/top-chat-model.nuxt.spec.ts new file mode 100644 index 00000000..3c8bdebc --- /dev/null +++ b/tests/top-chat-model.nuxt.spec.ts @@ -0,0 +1,130 @@ +// @vitest-environment nuxt +import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime' +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { ref } from 'vue' +import MetricsViewer from '../app/components/MetricsViewer.vue' +import MyUsageViewer from '../app/components/MyUsageViewer.vue' +import type { ReportDayTotals, UserTotals } from '../server/services/github-copilot-usage-api' + +const { mockUseFetch } = vi.hoisted(() => ({ + mockUseFetch: vi.fn(), +})) + +mockNuxtImport('useFetch', () => mockUseFetch) + +const modelFeature = (model: string, count: number) => ({ + model, + feature: 'chat_panel_ask_mode', + user_initiated_interaction_count: count, + code_generation_activity_count: 0, + code_acceptance_activity_count: 0, + loc_suggested_to_add_sum: 0, + loc_suggested_to_delete_sum: 0, + loc_added_sum: 0, + loc_deleted_sum: 0, +}) + +const reportDay = (totals_by_model_feature: ReturnType[]): ReportDayTotals => ({ + day: '2026-07-01', + organization_id: 'org1', + enterprise_id: 'ent1', + daily_active_users: 5, + weekly_active_users: 5, + monthly_active_users: 5, + monthly_active_agent_users: 1, + user_initiated_interaction_count: 128, + code_generation_activity_count: 0, + code_acceptance_activity_count: 0, + totals_by_ide: [], + totals_by_feature: [ + { + feature: 'chat_panel_ask_mode', + user_initiated_interaction_count: 128, + code_generation_activity_count: 0, + code_acceptance_activity_count: 0, + loc_suggested_to_add_sum: 0, + loc_suggested_to_delete_sum: 0, + loc_added_sum: 0, + loc_deleted_sum: 0, + }, + ], + totals_by_language_feature: [], + totals_by_language_model: [], + totals_by_model_feature, + loc_suggested_to_add_sum: 0, + loc_suggested_to_delete_sum: 0, + loc_added_sum: 0, + loc_deleted_sum: 0, +}) + +const userTotals = (totals_by_model_feature: ReturnType[]): UserTotals => ({ + login: 'octocat', + user_id: 1, + total_active_days: 1, + user_initiated_interaction_count: 128, + code_generation_activity_count: 0, + code_acceptance_activity_count: 0, + loc_suggested_to_add_sum: 0, + loc_suggested_to_delete_sum: 0, + loc_added_sum: 0, + loc_deleted_sum: 0, + totals_by_ide: [], + totals_by_feature: [], + totals_by_language_feature: [], + totals_by_model_feature, +}) + +describe('top chat model selection', () => { + beforeEach(() => { + mockUseFetch.mockReset() + }) + + it('MetricsViewer skips synthetic others bucket when picking the most used chat model', async () => { + const wrapper = await mountSuspended(MetricsViewer, { + props: { + metrics: [], + reportData: [ + reportDay([ + modelFeature('others', 100), + modelFeature('gpt-4.1', 25), + modelFeature('claude-3.7-sonnet', 3), + ]), + ], + }, + global: { + stubs: { + VMain: { template: '
' }, + }, + }, + }) + + expect(wrapper.text()).toContain('gpt-4.1') + expect(wrapper.text()).not.toContain('others') + }) + + it('MyUsageViewer skips synthetic Unknown bucket when picking the top model', async () => { + mockUseFetch.mockResolvedValue({ + data: ref({ + user: { login: 'octocat' }, + totals: userTotals([ + modelFeature('Unknown', 100), + modelFeature('claude-3.7-sonnet', 25), + ]), + dayRecords: [], + }), + pending: ref(false), + error: ref(null), + }) + + const wrapper = await mountSuspended(MyUsageViewer, { + global: { + stubs: { + VMain: { template: '
' }, + }, + }, + }) + + expect(wrapper.text()).toContain('claude-3.7-sonnet') + expect(wrapper.text()).not.toContain('Unknown') + }) +})