From fb718ce119ee5c3942e9a31048d0a0feab150084 Mon Sep 17 00:00:00 2001 From: Yaacov Date: Wed, 9 Sep 2026 09:17:48 +0300 Subject: [PATCH] fix(analytics): count latest explicit provider installations --- README.md | 5 +- scripts/product-insights-hogql.mjs | 19 +- scripts/product-insights.mjs | 33 +- scripts/product-insights.test.mjs | 30 +- workers/events/fixtures/contract-v4.json | 1509 ++++++++++++++++++++++ workers/events/src/conformance.test.ts | 30 +- workers/events/src/eventContract.test.ts | 4 +- workers/events/src/eventContract.ts | 14 +- 8 files changed, 1608 insertions(+), 36 deletions(-) create mode 100644 workers/events/fixtures/contract-v4.json diff --git a/README.md b/README.md index a773bf6..1c73e7a 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,10 @@ 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 3 is generated from +`workers/events/src/eventContract.ts`. Revision 4 is generated from `scient-desktop/packages/scient-analytics/src/wireContract.ts`, with a shared -conformance fixture for every registered event, plus revision-2 compatibility tests. Do not edit that copy +conformance fixture for every registered event, plus revision-2 and revision-3 +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. diff --git a/scripts/product-insights-hogql.mjs b/scripts/product-insights-hogql.mjs index 53587fe..bbaa14f 100644 --- a/scripts/product-insights-hogql.mjs +++ b/scripts/product-insights-hogql.mjs @@ -1,6 +1,6 @@ // 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())`; +AND properties.contractRevision IN ('3', '4') AND timestamp >= toStartOfDay(now()) - INTERVAL 30 DAY AND timestamp < toStartOfDay(now())`; const insight = (name, description, query) => ({ name, description, @@ -33,11 +33,18 @@ 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`, + "Latest observed provider installations", + "Latest explicit installed state per pseudonymous installation and provider. Bundled-but-missing providers and historical readiness are not counted as installed.", + `SELECT provider, countIf(installed = true) AS installations +FROM ( + SELECT distinct_id, properties.provider AS provider, + argMax(if(event = 'provider.installation.observed', properties.installed, properties.toInstalled), + tuple(timestamp, properties.event_id)) AS installed + FROM events WHERE ${population} AND properties.contractRevision = '4' + AND event IN ('provider.installation.observed', 'provider.installation.changed') + GROUP BY distinct_id, provider +) +GROUP BY provider HAVING installations > 0 ORDER BY installations DESC`, ), ]; diff --git a/scripts/product-insights.mjs b/scripts/product-insights.mjs index c21a958..d47a40b 100644 --- a/scripts/product-insights.mjs +++ b/scripts/product-insights.mjs @@ -6,12 +6,13 @@ import { fileURLToPath } from "node:url"; // 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 + SELECT event_id, event_name, distinct_id, occurred_at, 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' + AND json_extract(properties_json, '$.contractRevision') IN ('3', '4') ), views AS ( SELECT event_name, distinct_id, day, CASE event_name @@ -34,6 +35,24 @@ WITH eligible AS ( json_extract(p, '$.inputTokens') AS input_tokens, json_extract(p, '$.outputTokens') AS output_tokens FROM eligible WHERE event_name = 'provider.turn.usage' +), provider_installation_events AS ( + SELECT event_id, distinct_id, occurred_at, json_extract(p, '$.provider') AS provider, + CASE event_name + WHEN 'provider.installation.observed' THEN json_extract(p, '$.installed') + ELSE json_extract(p, '$.toInstalled') END AS installed + FROM eligible + WHERE json_extract(p, '$.contractRevision') = '4' + AND event_name IN ('provider.installation.observed', 'provider.installation.changed') +), latest_provider_installations AS ( + SELECT distinct_id, provider, installed + FROM ( + SELECT distinct_id, provider, installed, + row_number() OVER ( + PARTITION BY distinct_id, provider ORDER BY occurred_at DESC, event_id DESC + ) AS recency + FROM provider_installation_events + ) + WHERE recency = 1 ) SELECT 'feature_observations' AS metric, event_name || ':' || category AS category, count(*) AS installations, sum(observations) AS observations, @@ -52,18 +71,16 @@ 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') +SELECT 'provider_latest_observed_installed', provider, count(DISTINCT distinct_id), count(*), NULL, NULL +FROM latest_provider_installations WHERE installed = 1 +GROUP BY 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 +SELECT 'observed_product_population', 'revision-3-or-4', count(DISTINCT distinct_id), count(*), NULL, NULL FROM eligible ORDER BY metric, observations DESC, category `; diff --git a/scripts/product-insights.test.mjs b/scripts/product-insights.test.mjs index 8065eac..bbb89a3 100644 --- a/scripts/product-insights.test.mjs +++ b/scripts/product-insights.test.mjs @@ -2,7 +2,7 @@ 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", () => { +it("separates adoption, latest installation state, unknown tokens and failure populations", () => { const store = testDatabase(); try { const insert = store.sqlite.prepare( @@ -28,8 +28,28 @@ it("separates adoption, repeat days, readiness, unknown tokens and failure popul 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.installation.observed", + { provider: "codex", installed: true }, + { revision: "4", day: "2026-09-05T09:00:00Z" }, + ); + add( + "provider.installation.changed", + { provider: "codex", fromInstalled: true, toInstalled: false }, + { revision: "4", day: "2026-09-05T10:00:00Z" }, + ); + add( + "provider.installation.observed", + { provider: "pi", installed: false }, + { revision: "4", installation: "PRIVATE-2", day: "2026-09-05T09:00:00Z" }, + ); + add( + "provider.installation.changed", + { provider: "pi", fromInstalled: false, toInstalled: true }, + { revision: "4", installation: "PRIVATE-2", day: "2026-09-05T10:00:00Z" }, + ); + // Legacy readiness is not treated as an explicit current installation. + add("provider.discovered", { provider: "grok", state: "ready" }); add("provider.turn.usage", { provider: "codex", modelKey: "gpt-5.6-sol", @@ -59,7 +79,9 @@ it("separates adoption, repeat days, readiness, unknown tokens and failure popul 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.filter((r) => r.metric === "provider_latest_observed_installed")).toEqual([ + expect.objectContaining({ category: "pi", installations: 1, observations: 1 }), + ]); expect(rows.find((r) => r.metric === "provider_terminal_outcomes")).toMatchObject({ observations: 1, }); diff --git a/workers/events/fixtures/contract-v4.json b/workers/events/fixtures/contract-v4.json new file mode 100644 index 0000000..ed7cf7e --- /dev/null +++ b/workers/events/fixtures/contract-v4.json @@ -0,0 +1,1509 @@ +{ + "schemaVersion": 1, + "contractRevision": "4", + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "server.boot.heartbeat:fallbacks", + "name": "server.boot.heartbeat", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "server.boot.heartbeat:representative", + "name": "server.boot.heartbeat", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "provider.session.stopped:fallbacks", + "name": "provider.session.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "provider.session.stopped:representative", + "name": "provider.session.stopped", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "provider.installation.observed:fallbacks", + "name": "provider.installation.observed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "installed": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "provider.installation.observed:representative", + "name": "provider.installation.observed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "installed": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "provider.installation.changed:fallbacks", + "name": "provider.installation.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "other", + "fromInstalled": false, + "toInstalled": false, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "provider.installation.changed:representative", + "name": "provider.installation.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "provider": "antigravity", + "fromInstalled": false, + "toInstalled": true, + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "project.added:fallbacks", + "name": "project.added", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "method": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.added:representative", + "name": "project.added", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "method": "picker", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.add.failed:fallbacks", + "name": "project.add.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.add.failed:representative", + "name": "project.add.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.opened:fallbacks", + "name": "project.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "projectState": "unknown", + "initializationState": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.opened:representative", + "name": "project.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "projectState": "existing", + "initializationState": "initialized", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "project.initialization.failed:fallbacks", + "name": "project.initialization.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "project.initialization.failed:representative", + "name": "project.initialization.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "permission", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "thread.created:fallbacks", + "name": "thread.created", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "creationSource": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "thread.created:representative", + "name": "thread.created", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "creationSource": "new", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "thread.revert.completed:fallbacks", + "name": "thread.revert.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "boundaryClass": "checkpoint", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "thread.revert.completed:representative", + "name": "thread.revert.completed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "boundaryClass": "checkpoint", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "thread.revert.failed:fallbacks", + "name": "thread.revert.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "thread.revert.failed:representative", + "name": "thread.revert.failed", + "privacyLevel": "essential", + "consentLevel": "essential", + "properties": { + "failureClass": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "voice.transcription.cancelled:fallbacks", + "name": "voice.transcription.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "voice.transcription.cancelled:representative", + "name": "voice.transcription.cancelled", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "stage": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "surface.opened:fallbacks", + "name": "surface.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "surface": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "surface.opened:representative", + "name": "surface.opened", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "surface": "preview", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "panel.viewed:fallbacks", + "name": "panel.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "category": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "panel.viewed:representative", + "name": "panel.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "category": "browser", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "settings.viewed:fallbacks", + "name": "settings.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "section": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "settings.viewed:representative", + "name": "settings.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "section": "providers", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "usage.refresh.requested:fallbacks", + "name": "usage.refresh.requested", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "usage.refresh.requested:representative", + "name": "usage.refresh.requested", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "usage.availability:fallbacks", + "name": "usage.availability", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "state": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "usage.availability:representative", + "name": "usage.availability", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "state": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "feature.viewed:fallbacks", + "name": "feature.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "feature": "other", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "feature.viewed:representative", + "name": "feature.viewed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "feature": "search", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "case": "setting.changed:fallbacks", + "name": "setting.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "setting": "unknown", + "value": "unknown", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "case": "setting.changed:representative", + "name": "setting.changed", + "privacyLevel": "product", + "consentLevel": "product", + "properties": { + "setting": "direction", + "value": "rtl", + "appVersion": "0.6.8", + "buildChannel": "stable", + "contractRevision": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + }, + { + "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": "4" + } + } + ] +} diff --git a/workers/events/src/conformance.test.ts b/workers/events/src/conformance.test.ts index 2a915d5..f7607ea 100644 --- a/workers/events/src/conformance.test.ts +++ b/workers/events/src/conformance.test.ts @@ -1,21 +1,25 @@ import { describe, expect, it } from "vitest"; -import fixture from "../fixtures/contract-v3.json"; -import legacy from "../fixtures/contract-v2.json"; +import legacyV2 from "../fixtures/contract-v2.json"; +import legacyV3 from "../fixtures/contract-v3.json"; +import fixture from "../fixtures/contract-v4.json"; import { eventContractViolation, EVENT_DEFINITIONS, type PrivacyLevel } from "./eventContract"; -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(); +describe("desktop contract v4 conformance", () => { + it("continues accepting the complete revision 2 and 3 corpora", () => { + for (const legacy of [legacyV2, legacyV3]) { + for (const entry of legacy.cases) + expect( + eventContractViolation({ + ...entry, + privacyLevel: entry.privacyLevel as PrivacyLevel, + consentLevel: entry.consentLevel as PrivacyLevel, + }), + entry.case, + ).toBeNull(); + } }); it("accepts every generated desktop event including unknown-value fallbacks", () => { - expect(fixture.contractRevision).toBe("3"); + expect(fixture.contractRevision).toBe("4"); expect(new Set(fixture.cases.map((entry) => entry.name))).toEqual( new Set(Object.keys(EVENT_DEFINITIONS)), ); diff --git a/workers/events/src/eventContract.test.ts b/workers/events/src/eventContract.test.ts index fd3a272..9bfc02e 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-three registry deliberately bounded", () => { - expect(Object.keys(EVENT_DEFINITIONS)).toHaveLength(52); + it("keeps the revision-four registry deliberately bounded", () => { + expect(Object.keys(EVENT_DEFINITIONS)).toHaveLength(54); }); 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 d524288..ae6b1e8 100644 --- a/workers/events/src/eventContract.ts +++ b/workers/events/src/eventContract.ts @@ -215,6 +215,18 @@ export const EVENT_DEFINITIONS = { privacyLevel: "product", properties: { provider, runtimeSource, state: providerState }, }, + "provider.installation.observed": { + privacyLevel: "product", + properties: { provider, installed: { kind: "boolean" } }, + }, + "provider.installation.changed": { + privacyLevel: "product", + properties: { + provider, + fromInstalled: { kind: "boolean" }, + toInstalled: { kind: "boolean" }, + }, + }, "provider.readiness.changed": { privacyLevel: "product", properties: { provider, from: providerState, to: providerState }, @@ -658,7 +670,7 @@ export function eventContractViolation(input: { const rules: Readonly> = { appVersion, buildChannel, - contractRevision: { kind: "enum", values: ["1", "2", "3"], optional: true }, + contractRevision: { kind: "enum", values: ["1", "2", "3", "4"], optional: true }, ...definition.properties, }; for (const key of Object.keys(input.properties)) {