diff --git a/README.md b/README.md index a9bbb1e..4b860b8 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,9 @@ registered event name, its exact allowlisted property set, the event's declared privacy level, and sufficient explicit consent. Unknown events, extra properties, raw text, and mismatched consent or privacy classifications are rejected before storage. The versioned registry and its focused tests live in -`workers/events/src/eventContract.ts`. Revision 2 is generated from +`workers/events/src/eventContract.ts`. Revision 3 is generated from `scient-desktop/packages/scient-analytics/src/wireContract.ts`, with a shared -90-case conformance fixture for all 45 registered events. Do not edit that copy +conformance fixture for every registered event, plus revision-2 compatibility tests. Do not edit that copy independently; the desktop analytics document owns regeneration instructions. New events add an optional bounded `contractRevision`; legacy revision-1 payloads remain supported. Deploy this validator before releasing new producers. @@ -229,6 +229,22 @@ This command is an operational bridge, not a substitute for account authenticati ## PostHog dashboards +`bun run analytics:insights` reads an aggregate-only product report from D1: +feature observations/repeat days, provider/model terminal usage, reported token +counts and coverage, providers observed ready, and terminal outcomes. It uses +revision-3 Product/Diagnostic participants and the previous 30 complete UTC days. +Unknown token totals remain null; cache/reasoning subsets are not added again. +Ready observations are not a current sign-in inventory, installations are not +people, and observed population is not feature eligibility. See the desktop +analytics document for producer meanings and omissions. Existing delivery and +erasure diagnostics remain in `analytics:report`. + +The companion PostHog query definitions remain prepared, not installed or +live-qualified. Qualify their execution and project timezone before publishing; +match UTC to the D1 report when reconciling periods. Deploy the generated +revision-3 validator before releasing new desktop producers. No database +migration, collection-gate change or dashboard mutation is part of this extension. + The managed dashboard manifest is `scripts/posthog-dashboard-manifest.mjs`. It records all planned product dashboards, their source-backed queries, and the exact events each one needs. diff --git a/package.json b/package.json index 28667c7..40d2cbb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test": "vitest run", "db:migrate": "wrangler d1 migrations apply scientfactory-downloads --remote", "analytics:report": "node scripts/analytics-report.mjs", + "analytics:insights": "node scripts/product-insights.mjs", "analytics:dashboards": "node scripts/manage-posthog-dashboards.mjs", "analytics:reconcile": "node scripts/reconcile-analytics-pipeline.mjs", "identity:link": "node scripts/link-analytics-identity.mjs", diff --git a/scripts/posthog-dashboard-manifest.mjs b/scripts/posthog-dashboard-manifest.mjs index 8af3881..c3c573b 100644 --- a/scripts/posthog-dashboard-manifest.mjs +++ b/scripts/posthog-dashboard-manifest.mjs @@ -1,3 +1,5 @@ +import { providerUsageInsights, featureUsageInsights } from "./product-insights-hogql.mjs"; + const event = (name, customName = name, math = "total") => ({ kind: "EventsNode", event: name, @@ -331,6 +333,7 @@ GROUP BY current.week ORDER BY current.week`, name: "03 — Scient providers and agent runtime", phase: "planned", requiredEvents: [ + "provider.turn.usage", "provider.session.started", "provider.session.recovered", "provider.turn.completed", @@ -344,6 +347,7 @@ GROUP BY current.week ORDER BY current.week`, "Runtime-mode distribution", ], insights: [ + ...providerUsageInsights, preparedInsight( "Provider terminal outcomes", "Completed, failed and stopped turns for the same Product-consenting population, by provider. Stopped turns are not failures.", @@ -377,6 +381,9 @@ GROUP BY provider, failure_class ORDER BY failures DESC`, phase: "planned", requiredEvents: [ "surface.opened", + "panel.viewed", + "settings.viewed", + "usage.viewed", "project.initialization.completed", "thread.fork.completed", "voice.transcription.completed", @@ -390,6 +397,7 @@ GROUP BY provider, failure_class ORDER BY failures DESC`, "Fork and voice completion", ], insights: [ + ...featureUsageInsights, preparedInsight( "Feature completion by installation", "Unique installations completing bounded Scient feature outcomes.", diff --git a/scripts/product-insights-hogql.mjs b/scripts/product-insights-hogql.mjs new file mode 100644 index 0000000..53587fe --- /dev/null +++ b/scripts/product-insights-hogql.mjs @@ -0,0 +1,66 @@ +// Same Product population and complete-day window as product-insights.mjs. +const population = `properties.source = 'desktop' AND properties.consent_level IN ('product', 'diagnostic') +AND properties.contractRevision = '3' AND timestamp >= toStartOfDay(now()) - INTERVAL 30 DAY AND timestamp < toStartOfDay(now())`; +const insight = (name, description, query) => ({ + name, + description, + query: { kind: "HogQLQuery", query }, + aliases: [], +}); + +export const providerUsageInsights = [ + insight( + "Provider and model usage", + "Deduplicated live terminal usage reports, not model-picker clicks. Missing models are unknown; private and mixed-model labels use other. Counts include failed and stopped turns.", + `SELECT properties.provider AS provider, properties.modelKey AS model, +uniqExact(distinct_id) AS installations, uniqExact(properties.event_id) AS reported_turns +FROM events WHERE ${population} AND event = 'provider.turn.usage' +GROUP BY provider, model ORDER BY reported_turns DESC`, + ), + insight( + "Reported tokens and coverage", + "Main-agent reported counts only. Cache and reasoning subsets are not added again. Missing counts are not zero; complete/partial/unavailable turns show coverage. Not all-provider billing or subagent totals.", + `SELECT provider, +count() AS reported_turns, countIf(status = 'complete') AS complete_turns, +countIf(status = 'partial') AS partial_turns, countIf(status = 'unavailable') AS unavailable_turns, +count(input_tokens) AS input_reporting_turns, count(output_tokens) AS output_reporting_turns, +if(count(input_tokens) = 0, NULL, sum(input_tokens)) AS reported_input_tokens, +if(count(output_tokens) = 0, NULL, sum(output_tokens)) AS reported_output_tokens +FROM (SELECT properties.event_id AS id, any(properties.provider) AS provider, +any(properties.usageStatus) AS status, any(toFloat(properties.inputTokens)) AS input_tokens, +any(toFloat(properties.outputTokens)) AS output_tokens FROM events WHERE ${population} +AND event = 'provider.turn.usage' GROUP BY id) +GROUP BY provider ORDER BY reported_turns DESC`, + ), + insight( + "Providers observed ready", + "Installations with a provider observed ready during the window. Not signed-in people, current connection inventory, or proof the provider was used.", + `SELECT properties.provider AS provider, uniqExact(distinct_id) AS installations +FROM events WHERE ${population} AND ((event = 'provider.discovered' AND properties.state = 'ready') +OR (event = 'provider.readiness.changed' AND properties.to = 'ready')) GROUP BY provider ORDER BY installations DESC`, + ), +]; + +export const featureUsageInsights = [ + insight( + "Panel adoption and repeat days", + "Observed visible category entries, including a visible restored panel. Background tabs are excluded. Repeat means at least two distinct days, not runtime sessions; no eligibility-adjusted adoption rate is claimed.", + `SELECT category, count() AS installations, sum(observations) AS observations, countIf(days >= 2) AS repeat_installations +FROM (SELECT distinct_id, properties.category AS category, uniqExact(properties.event_id) AS observations, +uniqExact(toDate(timestamp)) AS days FROM events WHERE ${population} AND event = 'panel.viewed' +GROUP BY distinct_id, category) GROUP BY category ORDER BY installations DESC`, + ), + insight( + "Settings sections visited", + "Visible section entries, not preference changes or evidence of preference. Routes and search text are never collected.", + `SELECT properties.section AS section, uniqExact(distinct_id) AS installations, uniqExact(properties.event_id) AS observations +FROM events WHERE ${population} AND event = 'settings.viewed' GROUP BY section ORDER BY installations DESC`, + ), + insight( + "Usage tools viewed", + "Visible metric/range/breakdown combinations. Counts reflect view changes, not imported transcript usage or monetary spend.", + `SELECT properties.metric AS metric, properties.window AS range_days, properties.breakdown AS breakdown, +uniqExact(distinct_id) AS installations, uniqExact(properties.event_id) AS observations +FROM events WHERE ${population} AND event = 'usage.viewed' GROUP BY metric, range_days, breakdown ORDER BY observations DESC`, + ), +]; diff --git a/scripts/product-insights.mjs b/scripts/product-insights.mjs new file mode 100644 index 0000000..c21a958 --- /dev/null +++ b/scripts/product-insights.mjs @@ -0,0 +1,79 @@ +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Aggregate-only, occurrence-time report. No identities or free text returned. +// Full UTC days avoid comparing today's partial activity with complete days. +export const productInsightsQuery = ` +WITH eligible AS ( + SELECT event_name, distinct_id, date(occurred_at) AS day, properties_json AS p + FROM analytics_events + WHERE source = 'desktop' AND consent_level IN ('product', 'diagnostic') + AND julianday(occurred_at) >= julianday(date('now', '-30 days')) + AND julianday(occurred_at) < julianday(date('now')) + AND json_extract(properties_json, '$.contractRevision') = '3' +), views AS ( + SELECT event_name, distinct_id, day, + CASE event_name + WHEN 'panel.viewed' THEN json_extract(p, '$.category') + WHEN 'settings.viewed' THEN json_extract(p, '$.section') + WHEN 'feature.viewed' THEN json_extract(p, '$.feature') + WHEN 'usage.viewed' THEN json_extract(p, '$.metric') || ':' || json_extract(p, '$.window') || ':' || json_extract(p, '$.breakdown') + WHEN 'usage.availability' THEN json_extract(p, '$.state') + WHEN 'setting.changed' THEN json_extract(p, '$.setting') || ':' || json_extract(p, '$.value') + WHEN 'scient.operation.completed' THEN json_extract(p, '$.operationKind') + ELSE event_name END AS category + FROM eligible WHERE event_name IN ('panel.viewed', 'settings.viewed', 'feature.viewed', 'usage.viewed', 'usage.availability', 'usage.refresh.requested', 'setting.changed', 'scient.operation.completed', 'voice.transcription.completed') +), per_installation AS ( + SELECT event_name, category, distinct_id, count(*) AS observations, count(DISTINCT day) AS days + FROM views GROUP BY event_name, category, distinct_id +), tokens AS ( + SELECT distinct_id, json_extract(p, '$.provider') AS provider, + json_extract(p, '$.modelKey') AS model, + json_extract(p, '$.usageStatus') AS status, + json_extract(p, '$.inputTokens') AS input_tokens, + json_extract(p, '$.outputTokens') AS output_tokens + FROM eligible WHERE event_name = 'provider.turn.usage' +) +SELECT 'feature_observations' AS metric, event_name || ':' || category AS category, + count(*) AS installations, sum(observations) AS observations, + sum(days >= 2) AS repeat_installations, NULL AS value +FROM per_installation GROUP BY event_name, category +UNION ALL +SELECT 'provider_model_reported_turns', provider || ':' || model, count(DISTINCT distinct_id), count(*), NULL, NULL +FROM tokens GROUP BY provider, model +UNION ALL +SELECT 'provider_token_coverage', provider || ':' || status, count(DISTINCT distinct_id), count(*), NULL, NULL +FROM tokens GROUP BY provider, status +UNION ALL +SELECT 'reported_input_tokens', provider, count(DISTINCT distinct_id), count(input_tokens), NULL, sum(input_tokens) +FROM tokens GROUP BY provider +UNION ALL +SELECT 'reported_output_tokens', provider, count(DISTINCT distinct_id), count(output_tokens), NULL, sum(output_tokens) +FROM tokens GROUP BY provider +UNION ALL +SELECT 'provider_observed_ready', json_extract(p, '$.provider'), count(DISTINCT distinct_id), count(*), NULL, NULL +FROM eligible +WHERE event_name = 'provider.discovered' AND json_extract(p, '$.state') = 'ready' + OR event_name = 'provider.readiness.changed' AND json_extract(p, '$.to') = 'ready' +GROUP BY json_extract(p, '$.provider') +UNION ALL +SELECT 'provider_terminal_outcomes', json_extract(p, '$.provider') || ':' || event_name, + count(DISTINCT distinct_id), count(*), NULL, NULL +FROM eligible WHERE event_name IN ('provider.turn.completed', 'provider.turn.failed', 'provider.turn.stopped') +GROUP BY json_extract(p, '$.provider'), event_name +UNION ALL +SELECT 'observed_product_population', 'revision-3', count(DISTINCT distinct_id), count(*), NULL, NULL +FROM eligible +ORDER BY metric, observations DESC, category +`; + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const result = spawnSync( + "wrangler", + ["d1", "execute", "scientfactory-downloads", "--remote", "--command", productInsightsQuery], + { stdio: "inherit", timeout: 60_000 }, + ); + if (result.error) console.error("Product insights report failed or timed out"); + process.exitCode = result.error ? 1 : (result.status ?? 1); +} diff --git a/scripts/product-insights.test.mjs b/scripts/product-insights.test.mjs new file mode 100644 index 0000000..8065eac --- /dev/null +++ b/scripts/product-insights.test.mjs @@ -0,0 +1,70 @@ +import { expect, it } from "vitest"; +import { testDatabase } from "../workers/events/src/sqlite.testSupport.ts"; +import { productInsightsQuery } from "./product-insights.mjs"; + +it("separates adoption, repeat days, readiness, unknown tokens and failure populations", () => { + const store = testDatabase(); + try { + const insert = store.sqlite.prepare( + `INSERT INTO analytics_events (event_id, event_name, source, privacy_level, consent_level, occurred_at, distinct_id, properties_json) VALUES (?, ?, 'desktop', 'product', ?, ?, ?, ?)`, + ); + let id = 0; + const add = ( + name, + props, + { consent = "product", day = "2026-09-05", installation = "PRIVATE", revision = "3" } = {}, + ) => + insert.run( + String(++id), + name, + consent, + day, + installation, + JSON.stringify({ contractRevision: revision, ...props }), + ); + add("panel.viewed", { category: "browser" }); + add("panel.viewed", { category: "browser" }); + add("panel.viewed", { category: "browser" }, { day: "2026-09-04" }); + add("panel.viewed", { category: "browser" }, { consent: "essential" }); + add("panel.viewed", { category: "browser" }, { revision: "2" }); + add("panel.viewed", { category: "browser" }, { day: "2026-09-06" }); + add("provider.discovered", { provider: "codex", state: "ready" }); + add("provider.discovered", { provider: "pi", state: "unavailable" }); + add("provider.turn.usage", { + provider: "codex", + modelKey: "gpt-5.6-sol", + usageStatus: "complete", + inputTokens: 100, + outputTokens: 50, + }); + add("provider.turn.usage", { + provider: "codex", + modelKey: "unknown", + usageStatus: "unavailable", + }); + add("provider.turn.usage", { provider: "pi", modelKey: "other", usageStatus: "unavailable" }); + add("provider.turn.failed", { provider: "codex" }); + add("provider.turn.failed", { provider: "codex" }, { consent: "essential" }); + const rows = store.sqlite + .prepare(productInsightsQuery.replaceAll("'now'", "'2026-09-06'")) + .all(); + expect(rows.find((r) => r.metric === "feature_observations")).toMatchObject({ + installations: 1, + observations: 3, + repeat_installations: 1, + }); + expect( + rows.find((r) => r.metric === "reported_input_tokens" && r.category === "codex"), + ).toMatchObject({ observations: 1, value: 100 }); + expect( + rows.find((r) => r.metric === "reported_input_tokens" && r.category === "pi"), + ).toMatchObject({ observations: 0, value: null }); + expect(rows.filter((r) => r.metric === "provider_observed_ready")).toHaveLength(1); + expect(rows.find((r) => r.metric === "provider_terminal_outcomes")).toMatchObject({ + observations: 1, + }); + expect(JSON.stringify(rows)).not.toContain("PRIVATE"); + } finally { + store.close(); + } +}); diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro index 81e052f..359d03a 100644 --- a/src/pages/privacy.astro +++ b/src/pages/privacy.astro @@ -66,7 +66,7 @@ import Layout from "../layouts/Layout.astro";

You control analytics in Scient.

- Scient shares feature usage, failures and basic performance information to help improve the app. Sharing is on by default in supported releases; saved preferences are preserved. Turn it off or read “What’s shared?” in Settings → General → Privacy and analytics. Turning it back on includes usage, reliability and delivery counters. + Scient shares feature usage, provider/model categories, reported token counts, failures and basic performance information to help improve the app. Sharing is on by default in supported releases; saved preferences are preserved. Turn it off or read “What’s shared?” in Settings → General → Privacy and analytics. Turning it back on includes usage, reliability and delivery counters.

Analytics never collects conversations, file contents or names, paths, URLs, research results, credentials, provider account identities, or raw errors. There is no automatic click capture or session recording. Events use random installation identifiers. Your AI provider separately receives the content needed for your requests under its own privacy policy. diff --git a/workers/events/fixtures/contract-v3.json b/workers/events/fixtures/contract-v3.json new file mode 100644 index 0000000..d784000 --- /dev/null +++ b/workers/events/fixtures/contract-v3.json @@ -0,0 +1,1455 @@ +{ + "schemaVersion": 1, + "contractRevision": "3", + "cases": [ + { + "case": "app.session.started:fallbacks", + "name": "app.session.started", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "platform": "other", + "architecture": "other", + "contractRevision": "3" + } + }, + { + "case": "app.session.started:representative", + "name": "app.session.started", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "platform": "macos", + "architecture": "arm64", + "contractRevision": "3" + } + }, + { + "case": "app.session.ended:fallbacks", + "name": "app.session.ended", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "durationBucket": "unknown", + "shutdownClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "app.session.ended:representative", + "name": "app.session.ended", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "durationBucket": "5-15s", + "shutdownClass": "graceful", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "app.health:fallbacks", + "name": "app.health", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "component": "unknown", + "operation": "unknown", + "outcome": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "app.health:representative", + "name": "app.health", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "component": "server", + "operation": "startup", + "outcome": "completed", + "failureClass": "permission", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "app.diagnostics:fallbacks", + "name": "app.diagnostics", + "privacyLevel": "diagnostic", + "consentLevel": "diagnostic", + "properties": { + "queuedCountBucket": "unknown", + "droppedCountBucket": "unknown", + "retryCountBucket": "unknown", + "deliveryClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "app.diagnostics:representative", + "name": "app.diagnostics", + "privacyLevel": "diagnostic", + "consentLevel": "diagnostic", + "properties": { + "queuedCountBucket": "4-10", + "droppedCountBucket": "2-3", + "retryCountBucket": "1", + "deliveryClass": "network", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "server.boot.heartbeat:fallbacks", + "name": "server.boot.heartbeat", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "server.boot.heartbeat:representative", + "name": "server.boot.heartbeat", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.started:fallbacks", + "name": "provider.session.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "runtimeMode": "other", + "hasResumeCursor": false, + "hasCwd": false, + "hasModel": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.started:representative", + "name": "provider.session.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "runtimeMode": "full-access", + "hasResumeCursor": true, + "hasCwd": true, + "hasModel": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.recovered:fallbacks", + "name": "provider.session.recovered", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "strategy": "resume-thread", + "hasResumeCursor": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.recovered:representative", + "name": "provider.session.recovered", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "strategy": "adopt-existing", + "hasResumeCursor": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.stopped:fallbacks", + "name": "provider.session.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.session.stopped:representative", + "name": "provider.session.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.sessions.stopped_all:fallbacks", + "name": "provider.sessions.stopped_all", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "sessionCountBucket": "unknown", + "shutdownClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.sessions.stopped_all:representative", + "name": "provider.sessions.stopped_all", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "sessionCountBucket": "4-10", + "shutdownClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.runtime_mode.changed:fallbacks", + "name": "provider.runtime_mode.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "from": "other", + "to": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.runtime_mode.changed:representative", + "name": "provider.runtime_mode.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "from": "other", + "to": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.sent:fallbacks", + "name": "provider.turn.sent", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "modelFamily": "unknown", + "modelKey": "unknown", + "interactionMode": "unknown", + "runtimeMode": "other", + "attachmentCountBucket": "unknown", + "hasInput": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.sent:representative", + "name": "provider.turn.sent", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "modelFamily": "openai", + "modelKey": "gpt-5.6-sol", + "interactionMode": "plan", + "runtimeMode": "full-access", + "attachmentCountBucket": "1", + "hasInput": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.completed:fallbacks", + "name": "provider.turn.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "modelKey": "unknown", + "durationBucket": "unknown", + "usedTools": false, + "hasAttachment": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.completed:representative", + "name": "provider.turn.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "modelKey": "gpt-5.6-sol", + "durationBucket": "5-15s", + "usedTools": true, + "hasAttachment": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.failed:fallbacks", + "name": "provider.turn.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "provider": "other", + "modelKey": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.failed:representative", + "name": "provider.turn.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "provider": "antigravity", + "modelKey": "gpt-5.6-sol", + "failureClass": "unknown", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.stopped:fallbacks", + "name": "provider.turn.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "modelKey": "unknown", + "durationBucket": "unknown", + "stopClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.stopped:representative", + "name": "provider.turn.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "modelKey": "gpt-5.6-sol", + "durationBucket": "5-15s", + "stopClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.interrupted:fallbacks", + "name": "provider.turn.interrupted", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "initiator": "user", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.interrupted:representative", + "name": "provider.turn.interrupted", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "initiator": "user", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.request.responded:fallbacks", + "name": "provider.request.responded", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "requestKind": "approval", + "decision": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.request.responded:representative", + "name": "provider.request.responded", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "requestKind": "approval", + "decision": "approved", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.conversation.rolled_back:fallbacks", + "name": "provider.conversation.rolled_back", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "turnCountBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.conversation.rolled_back:representative", + "name": "provider.conversation.rolled_back", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "turnCountBucket": "2-3", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.discovered:fallbacks", + "name": "provider.discovered", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "runtimeSource": "unknown", + "state": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.discovered:representative", + "name": "provider.discovered", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "runtimeSource": "scient_managed", + "state": "ready", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.readiness.changed:fallbacks", + "name": "provider.readiness.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "from": "unknown", + "to": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.readiness.changed:representative", + "name": "provider.readiness.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "from": "unknown", + "to": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.runtime.source.changed:fallbacks", + "name": "provider.runtime.source.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "from": "unknown", + "to": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.runtime.source.changed:representative", + "name": "provider.runtime.source.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "from": "unknown", + "to": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.started:fallbacks", + "name": "provider.lifecycle.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "action": "unknown", + "runtimeSource": "unknown", + "stage": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.started:representative", + "name": "provider.lifecycle.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "action": "repair", + "runtimeSource": "scient_managed", + "stage": "downloading", + "failureClass": "permission", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.completed:fallbacks", + "name": "provider.lifecycle.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "action": "unknown", + "runtimeSource": "unknown", + "stage": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.completed:representative", + "name": "provider.lifecycle.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "action": "repair", + "runtimeSource": "scient_managed", + "stage": "downloading", + "failureClass": "permission", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.failed:fallbacks", + "name": "provider.lifecycle.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "provider": "other", + "action": "unknown", + "runtimeSource": "unknown", + "stage": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.failed:representative", + "name": "provider.lifecycle.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "provider": "antigravity", + "action": "repair", + "runtimeSource": "scient_managed", + "stage": "downloading", + "failureClass": "permission", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.cancelled:fallbacks", + "name": "provider.lifecycle.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "action": "unknown", + "runtimeSource": "unknown", + "stage": "unknown", + "failureClass": "unknown", + "durationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.lifecycle.cancelled:representative", + "name": "provider.lifecycle.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "action": "repair", + "runtimeSource": "scient_managed", + "stage": "downloading", + "failureClass": "permission", + "durationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.added:fallbacks", + "name": "project.added", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "method": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.added:representative", + "name": "project.added", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "method": "picker", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.add.failed:fallbacks", + "name": "project.add.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.add.failed:representative", + "name": "project.add.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.opened:fallbacks", + "name": "project.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "projectState": "unknown", + "initializationState": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.opened:representative", + "name": "project.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "projectState": "existing", + "initializationState": "initialized", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.initialization.completed:fallbacks", + "name": "project.initialization.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "outcome": "unknown", + "filesCreatedBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.initialization.completed:representative", + "name": "project.initialization.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "outcome": "unknown", + "filesCreatedBucket": "2-3", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.initialization.failed:fallbacks", + "name": "project.initialization.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "project.initialization.failed:representative", + "name": "project.initialization.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "permission", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.created:fallbacks", + "name": "thread.created", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "creationSource": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.created:representative", + "name": "thread.created", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "creationSource": "new", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.fork.completed:fallbacks", + "name": "thread.fork.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "workspaceMode": "unknown", + "boundaryClass": "unknown", + "refork": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.fork.completed:representative", + "name": "thread.fork.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "workspaceMode": "local", + "boundaryClass": "checkpoint", + "refork": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.fork.failed:fallbacks", + "name": "thread.fork.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "workspaceMode": "unknown", + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.fork.failed:representative", + "name": "thread.fork.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "workspaceMode": "local", + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.revert.completed:fallbacks", + "name": "thread.revert.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "boundaryClass": "checkpoint", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.revert.completed:representative", + "name": "thread.revert.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "boundaryClass": "checkpoint", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.revert.failed:fallbacks", + "name": "thread.revert.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "thread.revert.failed:representative", + "name": "thread.revert.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.started:fallbacks", + "name": "voice.transcription.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "engineClass": "other", + "languageMode": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.started:representative", + "name": "voice.transcription.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "engineClass": "local-whisper", + "languageMode": "automatic", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.completed:fallbacks", + "name": "voice.transcription.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "engineClass": "other", + "durationBucket": "unknown", + "audioDurationBucket": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.completed:representative", + "name": "voice.transcription.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "engineClass": "local-whisper", + "durationBucket": "5-15s", + "audioDurationBucket": "5-15s", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.failed:fallbacks", + "name": "voice.transcription.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "engineClass": "other", + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.failed:representative", + "name": "voice.transcription.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "engineClass": "local-whisper", + "failureClass": "permission", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.cancelled:fallbacks", + "name": "voice.transcription.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "voice.transcription.cancelled:representative", + "name": "voice.transcription.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "surface.opened:fallbacks", + "name": "surface.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "surface": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "surface.opened:representative", + "name": "surface.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "surface": "preview", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "panel.viewed:fallbacks", + "name": "panel.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "category": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "panel.viewed:representative", + "name": "panel.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "category": "browser", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "settings.viewed:fallbacks", + "name": "settings.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "section": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "settings.viewed:representative", + "name": "settings.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "section": "providers", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.viewed:fallbacks", + "name": "usage.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "metric": "other", + "window": "other", + "breakdown": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.viewed:representative", + "name": "usage.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "metric": "tokens", + "window": "7", + "breakdown": "model", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.refresh.requested:fallbacks", + "name": "usage.refresh.requested", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.refresh.requested:representative", + "name": "usage.refresh.requested", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.availability:fallbacks", + "name": "usage.availability", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "state": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "usage.availability:representative", + "name": "usage.availability", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "state": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "feature.viewed:fallbacks", + "name": "feature.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "feature": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "feature.viewed:representative", + "name": "feature.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "feature": "search", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.usage:fallbacks", + "name": "provider.turn.usage", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "modelKey": "unknown", + "terminalStatus": "other", + "usageStatus": "unavailable", + "usageScope": "main_agent", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "provider.turn.usage:representative", + "name": "provider.turn.usage", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "modelKey": "gpt-5.6-sol", + "terminalStatus": "other", + "usageStatus": "complete", + "usageScope": "main_agent", + "hasSubagents": false, + "inputTokens": 1500, + "outputTokens": 100, + "cachedInputTokens": 500, + "reasoningTokens": 50, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "setting.changed:fallbacks", + "name": "setting.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "setting": "unknown", + "value": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "setting.changed:representative", + "name": "setting.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "setting": "direction", + "value": "rtl", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.started:fallbacks", + "name": "scient.operation.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "other", + "trigger": "other", + "durationBucket": "unknown", + "failureClass": "unknown", + "reviewRequired": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.started:representative", + "name": "scient.operation.started", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "latex-build", + "trigger": "agent", + "durationBucket": "5-15s", + "failureClass": "permission", + "reviewRequired": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.completed:fallbacks", + "name": "scient.operation.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "other", + "trigger": "other", + "durationBucket": "unknown", + "failureClass": "unknown", + "reviewRequired": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.completed:representative", + "name": "scient.operation.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "latex-build", + "trigger": "agent", + "durationBucket": "5-15s", + "failureClass": "permission", + "reviewRequired": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.failed:fallbacks", + "name": "scient.operation.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "operationKind": "other", + "trigger": "other", + "durationBucket": "unknown", + "failureClass": "unknown", + "reviewRequired": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.failed:representative", + "name": "scient.operation.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "operationKind": "latex-build", + "trigger": "agent", + "durationBucket": "5-15s", + "failureClass": "permission", + "reviewRequired": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.cancelled:fallbacks", + "name": "scient.operation.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "other", + "trigger": "other", + "durationBucket": "unknown", + "failureClass": "unknown", + "reviewRequired": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.cancelled:representative", + "name": "scient.operation.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "latex-build", + "trigger": "agent", + "durationBucket": "5-15s", + "failureClass": "permission", + "reviewRequired": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.skipped:fallbacks", + "name": "scient.operation.skipped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "other", + "trigger": "other", + "durationBucket": "unknown", + "failureClass": "unknown", + "reviewRequired": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + }, + { + "case": "scient.operation.skipped:representative", + "name": "scient.operation.skipped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "operationKind": "latex-build", + "trigger": "agent", + "durationBucket": "5-15s", + "failureClass": "permission", + "reviewRequired": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "3" + } + } + ] +} diff --git a/workers/events/src/conformance.test.ts b/workers/events/src/conformance.test.ts index 087184b..2a915d5 100644 --- a/workers/events/src/conformance.test.ts +++ b/workers/events/src/conformance.test.ts @@ -1,10 +1,21 @@ import { describe, expect, it } from "vitest"; -import fixture from "../fixtures/contract-v2.json"; +import fixture from "../fixtures/contract-v3.json"; +import legacy from "../fixtures/contract-v2.json"; import { eventContractViolation, EVENT_DEFINITIONS, type PrivacyLevel } from "./eventContract"; -describe("desktop contract v2 conformance", () => { +describe("desktop contract v3 conformance", () => { + it("continues accepting the complete revision 2 corpus", () => { + for (const entry of legacy.cases) + expect( + eventContractViolation({ + ...entry, + privacyLevel: entry.privacyLevel as PrivacyLevel, + consentLevel: entry.consentLevel as PrivacyLevel, + }), + ).toBeNull(); + }); it("accepts every generated desktop event including unknown-value fallbacks", () => { - expect(fixture.contractRevision).toBe("2"); + expect(fixture.contractRevision).toBe("3"); expect(new Set(fixture.cases.map((entry) => entry.name))).toEqual( new Set(Object.keys(EVENT_DEFINITIONS)), ); diff --git a/workers/events/src/desktopPipeline.test.ts b/workers/events/src/desktopPipeline.test.ts index b3be72e..19a29df 100644 --- a/workers/events/src/desktopPipeline.test.ts +++ b/workers/events/src/desktopPipeline.test.ts @@ -87,9 +87,18 @@ it.skipIf(!desktopRoot)( trigger: "user", title: "PRIVATE-SOURCE", }); - expect(await runtime.flush()).toBe(4); + runtime.record("provider.turn.usage", { + provider: "codex", + model: "PRIVATE-MODEL", + terminalStatus: "completed", + usageStatus: "complete", + inputTokens: 12345, + outputTokens: 678, + cachedInputTokens: 200, + }); + expect(await runtime.flush()).toBe(5); expect(await runtime.pendingCount()).toBe(0); - expect(store.sqlite.prepare("SELECT count(*) AS n FROM analytics_events").get()!.n).toBe(4); + expect(store.sqlite.prepare("SELECT count(*) AS n FROM analytics_events").get()!.n).toBe(5); expect(uploads[0]!.body).not.toContain("PRIVATE"); const exportRequest = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); vi.stubGlobal("fetch", exportRequest); @@ -99,10 +108,21 @@ it.skipIf(!desktopRoot)( DESKTOP_POSTHOG_EXPORT_ENABLED: "true", POSTHOG_PROJECT_TOKEN: "synthetic", }), - ).toBe(4); + ).toBe(5); expect(exportRequest).toHaveBeenCalledOnce(); expect(String(exportRequest.mock.calls[0]![1].body)).not.toContain("PRIVATE"); const exported = JSON.parse(String(exportRequest.mock.calls[0]![1].body)); + expect( + exported.batch.find((event: { event: string }) => event.event === "provider.turn.usage") + .properties, + ).toMatchObject({ + provider: "codex", + modelKey: "other", + usageStatus: "complete", + inputTokens: 12345, + outputTokens: 678, + cachedInputTokens: 200, + }); expect( exported.batch.find( (event: { event: string }) => event.event === "provider.lifecycle.completed", diff --git a/workers/events/src/eventContract.test.ts b/workers/events/src/eventContract.test.ts index d499c18..fd3a272 100644 --- a/workers/events/src/eventContract.test.ts +++ b/workers/events/src/eventContract.test.ts @@ -3,8 +3,8 @@ import { describe, expect, it } from "vitest"; import { EVENT_DEFINITIONS, eventContractViolation } from "./eventContract"; describe("desktop event contract", () => { - it("keeps the revision-two registry deliberately bounded", () => { - expect(Object.keys(EVENT_DEFINITIONS)).toHaveLength(45); + it("keeps the revision-three registry deliberately bounded", () => { + expect(Object.keys(EVENT_DEFINITIONS)).toHaveLength(52); }); it("accepts the no-op outcome only with sufficient consent and bounded properties", () => { diff --git a/workers/events/src/eventContract.ts b/workers/events/src/eventContract.ts index 16e5aee..d524288 100644 --- a/workers/events/src/eventContract.ts +++ b/workers/events/src/eventContract.ts @@ -5,6 +5,7 @@ export const PRIVACY_LEVELS = ["essential", "product", "diagnostic", "contributi export type PrivacyLevel = (typeof PRIVACY_LEVELS)[number]; type PropertyRule = + | { readonly kind: "integer"; readonly max: number; readonly optional?: boolean } | { readonly kind: "boolean"; readonly optional?: boolean } | { readonly kind: "enum"; readonly values: ReadonlyArray; readonly optional?: boolean } | { readonly kind: "pattern"; readonly pattern: RegExp; readonly optional?: boolean }; @@ -69,6 +70,10 @@ const modelKey = { "gpt-5.6-sol", "gpt-5.6-terra", "grok-build", + "gemini-3.1-pro", + "gemini-3.7-flash", + "gemini-3.7-pro", + "gemini-3.8-flash", "openai/gpt-5", "other", "unknown", @@ -482,6 +487,90 @@ export const EVENT_DEFINITIONS = { }, }, }, + "panel.viewed": { + privacyLevel: "product", + properties: { + category: { + kind: "enum", + values: [ + "browser", + "terminal", + "files", + "file-preview", + "diff", + "pull-request", + "agents", + "sources", + "compute", + "source-pdf", + "generated-pdf", + "artifact", + "other", + ], + }, + }, + }, + "settings.viewed": { + privacyLevel: "product", + properties: { + section: { + kind: "enum", + values: [ + "general", + "appearance", + "projects", + "keybindings", + "providers", + "custom-models", + "voice", + "skills", + "integrations", + "scientific-computing", + "source-control", + "connections", + "archived", + "other", + ], + }, + }, + }, + "usage.viewed": { + privacyLevel: "product", + properties: { + metric: { kind: "enum", values: ["tokens", "cost", "limits", "other"] }, + window: { kind: "enum", values: ["1", "7", "30", "90", "other"] }, + breakdown: { kind: "enum", values: ["model", "time", "other"] }, + }, + }, + "usage.refresh.requested": { privacyLevel: "product", properties: {} }, + "feature.viewed": { + privacyLevel: "product", + properties: { + feature: { kind: "enum", values: ["search", "project-picker", "new-thread", "other"] }, + }, + }, + "usage.availability": { + privacyLevel: "product", + properties: { + state: { kind: "enum", values: ["available", "partial", "unavailable", "other"] }, + }, + }, + "provider.turn.usage": { + privacyLevel: "product", + properties: { + provider, + modelKey, + terminalStatus: { kind: "enum", values: ["completed", "failed", "stopped", "other"] }, + usageStatus: { kind: "enum", values: ["complete", "partial", "unavailable"] }, + usageScope: { kind: "enum", values: ["main_agent"] }, + hasSubagents: { kind: "boolean", optional: true }, + inputTokens: { kind: "integer", max: 1_000_000_000, optional: true }, + outputTokens: { kind: "integer", max: 1_000_000_000, optional: true }, + cachedInputTokens: { kind: "integer", max: 1_000_000_000, optional: true }, + cacheCreationTokens: { kind: "integer", max: 1_000_000_000, optional: true }, + reasoningTokens: { kind: "integer", max: 1_000_000_000, optional: true }, + }, + }, "setting.changed": { privacyLevel: "product", properties: { @@ -528,6 +617,14 @@ const PRIVACY_RANK: Readonly> = { }; function propertyViolation(key: string, value: unknown, rule: PropertyRule): string | null { + if (rule.kind === "integer") { + return typeof value === "number" && + Number.isSafeInteger(value) && + value >= 0 && + value <= rule.max + ? null + : `Invalid event property '${key}'`; + } if (rule.kind === "boolean") { return typeof value === "boolean" ? null : `Invalid event property '${key}'`; } @@ -561,7 +658,7 @@ export function eventContractViolation(input: { const rules: Readonly> = { appVersion, buildChannel, - contractRevision: { kind: "enum", values: ["1", "2"], optional: true }, + contractRevision: { kind: "enum", values: ["1", "2", "3"], optional: true }, ...definition.properties, }; for (const key of Object.keys(input.properties)) { @@ -578,5 +675,36 @@ export function eventContractViolation(input: { const violation = propertyViolation(key, value, rule); if (violation) return violation; } + if (input.name === "provider.turn.usage") { + const p = input.properties; + const counts = [ + "inputTokens", + "outputTokens", + "cachedInputTokens", + "cacheCreationTokens", + "reasoningTokens", + ]; + if ( + p.usageStatus === "complete" && + (p.inputTokens === undefined || p.outputTokens === undefined) + ) + return "Incomplete token totals"; + if (p.usageStatus === "unavailable" && counts.some((key) => p[key] !== undefined)) + return "Unavailable usage includes counts"; + for (const [subset, total] of [ + ["cachedInputTokens", "inputTokens"], + ["cacheCreationTokens", "inputTokens"], + ["reasoningTokens", "outputTokens"], + ] as const) { + const subsetValue = p[subset]; + const totalValue = p[total]; + if ( + typeof subsetValue === "number" && + typeof totalValue === "number" && + subsetValue > totalValue + ) + return "Token subset exceeds total"; + } + } return null; }