Migration to async Copilot Usage Metrics API - Full Implementation (All Phases Complete)#299
Migration to async Copilot Usage Metrics API - Full Implementation (All Phases Complete)#299
Conversation
Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
API_MIGRATION_DESIGN.md
Outdated
|
|
||
| ## Database Design | ||
|
|
||
| ### Why PostgreSQL? |
There was a problem hiding this comment.
@copilot can we use nitro abstractions for DB calls?
so that users can use PGSQL but also other DBs if they implement the API?
Just to make it configurable?
There was a problem hiding this comment.
@copilot address the comments
There was a problem hiding this comment.
Yes! Updated design to use Nitro's unstorage abstraction. This makes storage configurable - users can choose PostgreSQL, Redis, MongoDB, filesystem, or any unstorage driver by updating nuxt.config.ts. PostgreSQL remains the recommended default for its JSONB support and query capabilities.
The storage interface is now database-agnostic, aligning with the existing TODO comment in server/api/metrics.ts:5 that references unstorage.
See updated sections on "Storage Abstraction with Nitro Unstorage" and Phase 1 changes. (Commit: 5194ac0)
| - `server/services/data-transformer.ts` - Transform new API schema to DB model | ||
| - `shared/utils/metrics-util-v2.ts` - New metrics utility for async operations | ||
|
|
||
| ### Phase 3: Data Sync Service (Week 5-6) |
There was a problem hiding this comment.
@copilot can this run in a separate container and be designed as a job?
There was a problem hiding this comment.
Yes! Added comprehensive design for running sync as a separate container/job. Two deployment options:
Option A (Integrated): Sync runs inside main app via Nitro scheduled tasks - simpler for MVP.
Option B (Separate Container): Sync runs as independent Kubernetes CronJob or Docker service - recommended for production. Benefits: independent scaling, resource isolation, web availability during sync failures.
Includes Dockerfile.sync, standalone entry point (server/sync-entry.ts), K8s CronJob YAML, and docker-compose examples. See updated Phase 5 and Kubernetes Deployment sections. (Commit: 5194ac0)
…ontainer support Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
|
@copilot implement the suggested plan in this branch, add tests, make sure that everything works. Mock out GithubAPI where needed for tests. Let me know if you need actual sample data, I can commit to this branch |
Starting implementation of the migration plan. I'll implement this incrementally following the phases in the design doc: Phase 1: Storage infrastructure with unstorage abstraction I'll begin with Phase 1, implementing the storage abstraction layer with filesystem driver for development (can be switched to PostgreSQL via config). Will add comprehensive tests at each step and mock GitHub API calls. |
- Add unstorage dependency for database-agnostic persistence - Create storage type definitions and interfaces - Implement metrics-storage, seats-storage, and sync-storage modules - Configure filesystem driver in nuxt.config.ts for development - Add initial storage tests (need Nuxt runtime context to run) Storage layer supports PostgreSQL, Redis, MongoDB, filesystem via config Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
- Implement GitHub Copilot Usage Metrics API client for async downloads - Add NDJSON parsing functionality - Create mock API implementation for testing - Support both real and mock modes via runtime config - Add comprehensive tests for API client and NDJSON parsing All tests passing (9/9) Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
|
@copilot work on all phases 1-6 - keep updating the docs with implementation progress and any changes made (or when design had to be altered) |
Continuing with Phases 3-6 implementation. I'll complete the sync service, API layer updates, background scheduler, and comprehensive testing. Will update docs with any implementation changes and progress. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Phase 3: Data Sync Service - Implement core sync service with gap detection - Add admin endpoints for manual sync trigger and status monitoring - Support single-date and date-range synchronization Phase 4: API Layer Updates - Create metrics-util-v2 with storage and new API support - Update metrics endpoint with feature flags (USE_NEW_API, ENABLE_HISTORICAL_MODE) - Maintain backward compatibility with legacy API Phase 5: Background Scheduler - Add daily-sync scheduled task (Nitro) - Create standalone sync-entry.ts for separate container deployment - Add Dockerfile.sync for containerized sync jobs - Create k8s/cronjob.yaml for Kubernetes deployments - Add docker-compose.yml for dev environment All 106 existing tests passing Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com>
| logger.error('Error getting sync status:', error); | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| return new Response( | ||
| JSON.stringify({ error: 'Failed to get sync status', message: errorMessage }), |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 days ago
In general, to fix this type of issue, you should avoid returning raw error messages or stack traces from caught exceptions to the client. Instead, log the full error on the server (for developers/ops) and return a generic, non-sensitive message to the client. Only deliberately curated, user-safe error information should be sent in HTTP responses.
For this specific file, we should keep the server-side logging (logger.error('Error getting sync status:', error);) so operators can see the detailed reason for failure, but we should stop including errorMessage in the JSON response. We can simply return a generic message such as "An internal error occurred" or reuse the existing error: 'Failed to get sync status' field and omit the detailed message. This preserves existing behavior as much as possible (same status code, response structure similar in spirit) while no longer leaking internal details. Concretely, in server/api/admin/sync-status.get.ts, within the catch block (lines 60–66), remove the computation of errorMessage and change the JSON.stringify call so that it no longer includes the tainted data, e.g.:
return new Response(
JSON.stringify({ error: 'Failed to get sync status' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);No new methods or imports are strictly required; we just tighten the response payload to exclude error-derived content.
| @@ -59,9 +59,8 @@ | ||
|
|
||
| } catch (error: unknown) { | ||
| logger.error('Error getting sync status:', error); | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| return new Response( | ||
| JSON.stringify({ error: 'Failed to get sync status', message: errorMessage }), | ||
| JSON.stringify({ error: 'Failed to get sync status' }), | ||
| { status: 500, headers: { 'Content-Type': 'application/json' } } | ||
| ); | ||
| } |
| logger.error('Error in sync endpoint:', error); | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| return new Response( | ||
| JSON.stringify({ error: 'Sync failed', message: errorMessage }), |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix information exposure via stack traces or error messages, the detailed error (including message and stack) should be logged only on the server, while the HTTP response sent back to the client should contain a generic, non-sensitive error description. Any data derived directly from the caught error object should not be included in the response unless it is explicitly sanitized.
For this specific code, the best fix is to keep logger.error('Error in sync endpoint:', error); so that full information remains available in server logs, but stop returning errorMessage to the client. Instead, respond with a static, generic error JSON such as { error: 'Sync failed' } or perhaps a minimal, non-sensitive code. This preserves existing behavior in terms of HTTP status (500) and JSON structure (still an object with an error field), while removing the sensitive message field that depends on the thrown error. Concretely, in server/api/admin/sync.post.ts, lines 145–148 should be updated so that errorMessage is not computed or used in the response body, and JSON.stringify is called with a constant object.
No new imports or helper methods are required; we simply remove the tainted data from the response.
| @@ -142,9 +142,8 @@ | ||
|
|
||
| } catch (error: unknown) { | ||
| logger.error('Error in sync endpoint:', error); | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| return new Response( | ||
| JSON.stringify({ error: 'Sync failed', message: errorMessage }), | ||
| JSON.stringify({ error: 'Sync failed' }), | ||
| { status: 500, headers: { 'Content-Type': 'application/json' } } | ||
| ); | ||
| } |
- Replace USE_NEW_API with COPILOT_METRICS_API env var
- Pass env vars from host via ${VAR:-default} syntax
- Add playwright service (profile: test) for E2E testing
- Fix playwright docker config to set NITRO_PORT=3000
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Middleware: Skip auth for metrics/seats when ENABLE_HISTORICAL_MODE=true (set empty headers, let handler decide based on storage availability) - Middleware: Skip auth for admin routes (admin endpoints handle auth internally) - metrics-util-v2: Rewrite decision tree: 1. Mock mode (env var IS_DATA_MOCKED) → mock data, no DB, no API 2. Historical mode (ENABLE_HISTORICAL_MODE) → DB with sync-on-miss 3. Direct API mode → fetch from GitHub API - metrics-util-v2: Default to last 28 days when no date range specified - Admin sync endpoint: Use isMockMode() env check instead of per-request param - Options.ts: Fix mock detection to check === true or === 'true' (not truthy) - Docker compose: Add sync-seed and playwright-storage services for full pipeline E2E testing with shared volume - E2E test: Two-phase storage pipeline test: Phase 1 (@seed): Sync mock data to shared volume Phase 2 (@storage): Dashboard reads from DB without GitHub token Tested: 147 unit tests pass, 123 mock E2E tests pass, 12 storage pipeline E2E tests pass (6 seed + 6 storage, 3 browsers each) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add comprehensive header comments to docker-compose.yml with quick start, testing commands, services overview, and environment variables reference - Add inline comments for each service explaining purpose and modes - Add 'Scenario 4: Docker Compose' section to DEPLOYMENT.md covering: local development (mock, real API, historical mode), sync service, E2E tests (mock and storage pipeline), services table, cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove hard depends_on db from web service (DB not needed for mock/direct API) - Update Quick Start: 'docker compose up web' for mock and direct API modes, 'docker compose up web db' only for historical mode - Add note in DEPLOYMENT.md clarifying when db service is needed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matches the main Dockerfile base image. Fixes npm ci failure caused by peer dependency version mismatch between npm 10 (node 20) and the lockfile generated by npm 11 (node 24). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace useStorage() with getStorage() helper that falls back to unstorage+fsDriver when running outside Nitro context (sync-entry.ts) - Fix isMockMode() to fall back to process.env when useRuntimeConfig is unavailable in standalone mode - Remove USER node from Dockerfile.sync for volume write access - Add sync_data volume mount to web service for historical mode - Tested: sync stores 28 days, web reads from DB successfully Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pg_isready defaults to checking a database matching the username, causing FATAL errors. Specify -d copilot_metrics explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the filesystem-based unstorage layer with PostgreSQL: - Add server/storage/db.ts: connection pool, schema init (metrics, sync_status, seats tables with JSONB columns and indexes) - Rewrite metrics-storage.ts: single SQL query for date ranges instead of N file reads, proper upsert with ON CONFLICT - Rewrite sync-storage.ts and seats-storage.ts with SQL - Add server/plugins/db-init.ts: auto-init schema on startup - Add tests/pg-storage.spec.ts: 19 tests using pg-mem (in-memory PG) - Fix import for standalone sync container (ofetch fallback) - Fix ofetch responseType for report downloads - Wire DATABASE_URL through docker-compose for all services Tested: sync saves 26 days to PG, web reads from PG, all 161 unit tests + 123 E2E tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix report-transformer to filter code_completion feature only for suggestion/acceptance metrics (was including agent_edit, chat) - Fix model mapping to scope by feature type (code_completion vs chat) - Add PR types (ReportPullRequests, ReportCliTotals) to API types - Update UI labels: 'Suggestions' → 'Code Completions' across all views - Update chat labels: 'Turns' → 'Chat Interactions', 'Acceptances' → 'Code Actions' - Add AgentActivityViewer component: agent LOC charts, feature breakdown, model-feature table (showing claude-opus, gpt-5.3-codex, etc.) - Add PullRequestViewer component: PRs created/reviewed/merged, Copilot contributions, review suggestions - Pass raw ReportDayTotals through API response for new components - Add getReportDataByDateRange() for DB queries - Add comprehensive acceptance rate test with real-world data structure Acceptance rate was 0.4% because transformer mapped ALL features' activity counts as 'suggestions'. Now correctly filters to code_completion only, yielding ~15-40% rates matching real data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sertions - Mock mode now uses generateMockReport() → transformReportToMetrics() when COPILOT_METRICS_API=new, exercising the same code path as production - Legacy mock mode still reads old JSON files when COPILOT_METRICS_API=legacy - Added acceptance rate assertion (5-80% range) to catch the 0.4% regression - Added suggestion count assertion to verify non-zero completions - Updated E2E locators to match new UI labels: - 'Total Code Completions' (was 'Total count of Suggestions (Prompts)') - 'Cumulative Chat Interactions' (was 'Cumulative Number of Turns') - 'Top 5 Languages by accepted code completions' (was 'by accepted suggestions') - All 27 E2E tests pass, all 162 unit tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The web container can start before PostgreSQL is fully ready to accept
connections, even with depends_on/service_healthy. Added retry logic
(up to 10 attempts with linear backoff) for PG error code 57P03
('database system is starting up') and ECONNREFUSED.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mock mode now simulates the complete two-step GitHub API flow:
1. requestDownloadLinks() → returns URLs to localhost static files
2. downloadReport() → fetches the report via HTTP (no special mock bypass)
New mock data files in public/mock-data/new-api/:
- organization-28-day-report.json — anonymized real API data from cody-test-org
(26 days, 12 models, 6 features, pull request data, 30.4% acceptance rate)
- enterprise-28-day-report.json — enterprise scope variant
- organization-1-day-report.json — single day report
- download-links-*.json — mock API responses pointing to local files
Key changes:
- mockRequestDownloadLinks() returns localhost URLs (port-aware via env)
- downloadReport() no longer has mock bypass — always fetches via HTTP
- generateMockReport() reads from static file, falls back to generator
- metrics-util-v2 mock path calls fetchLatestReport() for full pipeline
- Unit tests updated for new mock API
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
totals_by_language_model has no 'feature' field, so it included LOC from all features (agent_edit adds thousands of lines). Switched to using totals_by_language_feature filtered to code_completion, which correctly scopes lines suggested/accepted. Real data: 2,235 lines suggested → 811 accepted (36.3%) Bug showed: 12,749 suggested → 71,188 accepted (558%) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- total_engaged_users on copilot_ide_chat was hardcoded to 0; now uses monthly_active_chat_users from the report day data - total_chat_insertion_events was hardcoded to 0; now maps code_generation_activity_count from chat features (code generated in chat responses that users can insert/apply) - total_chat_copy_events already used code_acceptance_activity_count Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
/api/github-stats was calling getMetricsData() (legacy) instead of getMetricsDataV2(). Switched to v2 so it gets the same CopilotMetrics data from the new download-based API, including IDE chat models and code completion models. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- /api/github-stats now uses getMetricsDataV2 and returns reportData stats (model-feature table, feature/model summaries, active users) - AgentModeViewer shows rich model usage tables when reportData available, falls back to legacy expansion panels for old API - hasMetrics() now requires report_data IS NOT NULL so sync will re-download days missing raw report data - Updated pg-storage tests to include reportData fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flip the flag semantics: new API is now the default with zero config. Legacy API requires explicit USE_LEGACY_API=true opt-in. This ensures: - No legacy API calls are made unless explicitly opted in - New implementation is fully exercised by default - Mock mode, historical mode, and direct mode all use new API - Legacy path only reachable via USE_LEGACY_API=true Updated: metrics-util-v2.ts, nuxt.config.ts, docker-compose.yml, .env.example, tests, and all documentation references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // Build models from model_feature data (code_completion only) | ||
| const modelNames = [...new Set(completionModels.map(m => m.model))]; | ||
| const models = modelNames.map(modelName => { | ||
| const modelData = completionModels.find(m => m.model === modelName); |
| closePool: vi.fn(), | ||
| })); | ||
|
|
||
| import { initSchema } from '../server/storage/db'; |
| import { | ||
| saveSyncStatus, getSyncStatus, createPendingSyncStatus, | ||
| markSyncInProgress, markSyncCompleted, markSyncFailed, | ||
| getPendingSyncs, getFailedSyncs, | ||
| } from '../server/storage/sync-storage'; |
| */ | ||
|
|
||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import { generateMockReport, mockRequestDownloadLinks } from '../server/services/github-copilot-usage-api-mock'; |
|
|
||
| // Import sync service AFTER mocks are set up | ||
| import { syncBulk, syncMetricsForDate, syncMetricsForDateRange, detectGaps, syncGaps, getSyncStats } from '../server/services/sync-service'; | ||
| import { saveMetrics, getMetrics, hasMetrics, getMetricsByDateRange, getReportData } from '../server/storage/metrics-storage'; |
| it('should skip already-synced days', async () => { | ||
| // First sync | ||
| await syncBulk('organization', 'test-org', TEST_HEADERS); | ||
| const firstCallCount = (saveMetrics as any).mock.calls.length; |
| it('should store and retrieve CopilotMetrics via in-memory mock', async () => { | ||
| // Generate mock report | ||
| const report = generateMockReport('2026-02-01', '2026-02-03'); | ||
| const metrics = transformReportToMetrics(report); |
- Rewrite GitHubTab page object for new AgentModeViewer component - Scope card locators to .github-com-container to avoid false matches - Fix Chat card locator: use v-card-title child selector instead of regex ^Chat$ which failed against multi-line card content - Add explicit waits for github-stats API data in beforeAll - Configure github-tab tests as serial with shared page - Update DashboardPage.gotoGitHubTab() to wait for content load - All 47 E2E tests pass consistently (5/5 runs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Upgrade immutable from 5.0.3 to 5.1.5 (via sass-embedded dependency) to fix high-severity prototype pollution vulnerability in mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs. Also patches GHSA-xpqw-6gx7-v673 (svgo DoS via entity expansion). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- playwright.yml: Add test-docker-sync job to verify Dockerfile.sync builds on PRs and pushes to main - deploy_to_ghcr.yml: Add push_sync_to_ghcr job to build and push sync image as ghcr.io/<repo>-sync on push to main - deploy_to_ghcr_tag_release.yaml: Add push_sync_to_ghcr job to build and push sync image on release with version tags Sync image is published as a separate package (-sync suffix) with the same tag strategy as the main image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Playwright CI fixes: - Add test-storage-pipeline job with PostgreSQL 16 service container Phase 1 (@seed): seeds DB with mock data via admin/sync API Phase 2 (@storage): verifies dashboard reads from DB - Exclude @seed/@storage from test and test-docker-playwright jobs (these need PostgreSQL, not available in those jobs) - Fix teams-comparison.spec.ts: add serial mode and reorder tests so 'empty state' runs before 'team selection' (shared page state) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: Phase 1 started the server with DATABASE_URL but without ENABLE_HISTORICAL_MODE=true, so the db-init plugin skipped schema creation. The sync API then failed with 'relation metrics does not exist'. Fix: - db-init plugin now also runs when DATABASE_URL is set (not just ENABLE_HISTORICAL_MODE) — prevents this class of bug - Also added ENABLE_HISTORICAL_MODE=true to Phase 1 workflow step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…in permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
The auth middleware was applying GitHub token authentication to /api/_auth/session, causing 500 errors in storage-only mode where no GitHub token is configured. The _auth/session endpoint is handled by nuxt-auth-utils and doesn't need GitHub API headers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🎉 PRODUCTION READY
Final Status
Test Coverage: 118 tests passing (100% pass rate)
Quality Assurance:
Latest Update
Package Lock Resolution (Commit: [current]):
Code Review Fixes Applied
Round 1 (Commits: e1a65d2-b8e81c0):
Round 2 (Commit: c62fe0f):
Security Notice
/api/admin/*) are NOT authenticated by default.You MUST secure these endpoints via:
See MIGRATION_GUIDE.md for details and nginx example.
Implementation Complete
All 6 phases delivered:
Migration Paths
Option 1: Keep Legacy (until April 2026)
# Backward compatible - no changes neededOption 2: Enable New API
Option 3: Full Migration (recommended)
See MIGRATION_GUIDE.md for complete instructions
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.