From a076604902bf78cf780e63a055a3f006bbb92666 Mon Sep 17 00:00:00 2001 From: Yaacov Date: Wed, 9 Sep 2026 09:17:47 +0300 Subject: [PATCH] fix(analytics): report explicit provider installation state --- .../ProviderLifecycleAnalytics.test.ts | 77 +- .../telemetry/ProviderLifecycleAnalytics.ts | 60 +- docs/internals/product-analytics.md | 10 +- .../fixtures/contract-v4.json | 1509 +++++++++++++++++ .../scient-analytics/src/conformance.test.ts | 17 +- packages/scient-analytics/src/conformance.ts | 3 + .../scient-analytics/src/contract.test.ts | 2 +- packages/scient-analytics/src/contract.ts | 26 +- .../src/generateConformance.ts | 2 +- packages/scient-analytics/src/wireContract.ts | 14 +- 10 files changed, 1697 insertions(+), 23 deletions(-) create mode 100644 packages/scient-analytics/fixtures/contract-v4.json diff --git a/apps/server/src/telemetry/ProviderLifecycleAnalytics.test.ts b/apps/server/src/telemetry/ProviderLifecycleAnalytics.test.ts index 824aea775..b6e5c338d 100644 --- a/apps/server/src/telemetry/ProviderLifecycleAnalytics.test.ts +++ b/apps/server/src/telemetry/ProviderLifecycleAnalytics.test.ts @@ -87,11 +87,51 @@ describe("Provider lifecycle analytics", () => { it("does not replay terminal or pre-consent operations as new successes or failures", () => { const mapper = createProviderLifecycleAnalyticsMapper(); expect(mapper.observe([provider(operation("failed"))]).map((event) => event.name)).toEqual([ - "provider.discovered", + "provider.installation.observed", ]); mapper.clear(); expect(mapper.observe([provider(operation("failed"))]).map((event) => event.name)).toEqual([ - "provider.discovered", + "provider.installation.observed", + ]); + }); + + it("reports explicit installed state without treating bundled provider entries as installs", () => { + const mapper = createProviderLifecycleAnalyticsMapper(); + const installed = provider(); + const { connection: _connection, ...withoutConnection } = installed; + const missing = { + ...withoutConnection, + enabled: false, + installed: false, + status: "disabled" as const, + }; + + expect(mapper.observe([missing])).toEqual([ + { + name: "provider.installation.observed", + properties: { + provider: "droid", + installed: false, + }, + }, + ]); + expect(mapper.observe([installed])).toEqual([ + { + name: "provider.installation.changed", + properties: { + provider: "droid", + fromInstalled: false, + toInstalled: true, + }, + }, + { + name: "provider.readiness.changed", + properties: { provider: "droid", from: "disabled", to: "ready" }, + }, + { + name: "provider.runtime.source.changed", + properties: { provider: "droid", from: "missing", to: "scient_managed" }, + }, ]); }); @@ -106,6 +146,14 @@ describe("Provider lifecycle analytics", () => { status: "warning" as const, }; expect(mapper.observe([missing])).toEqual([ + { + name: "provider.installation.changed", + properties: { + provider: "droid", + fromInstalled: true, + toInstalled: false, + }, + }, { name: "provider.readiness.changed", properties: { provider: "droid", from: "ready", to: "warning" }, @@ -116,4 +164,29 @@ describe("Provider lifecycle analytics", () => { }, ]); }); + + it("aggregates multiple instances without exposing instance identifiers", () => { + const mapper = createProviderLifecycleAnalyticsMapper(); + const installed = provider(); + const { connection: _connection, ...withoutConnection } = installed; + const missing = { + ...withoutConnection, + instanceId: ProviderInstanceId.make("another-private-name"), + installed: false, + status: "disabled" as const, + }; + + expect(mapper.observe([missing, installed])).toEqual([ + { + name: "provider.installation.observed", + properties: { provider: "droid", installed: true }, + }, + ]); + const events = mapper.observe([missing]); + expect(JSON.stringify(events)).not.toContain("private-name"); + expect(events).toContainEqual({ + name: "provider.installation.changed", + properties: { provider: "droid", fromInstalled: true, toInstalled: false }, + }); + }); }); diff --git a/apps/server/src/telemetry/ProviderLifecycleAnalytics.ts b/apps/server/src/telemetry/ProviderLifecycleAnalytics.ts index 39db1aae8..1c37c42c6 100644 --- a/apps/server/src/telemetry/ProviderLifecycleAnalytics.ts +++ b/apps/server/src/telemetry/ProviderLifecycleAnalytics.ts @@ -12,6 +12,7 @@ interface Operation { } interface Snapshot { + readonly installed: boolean; readonly source: string; readonly state: string; readonly runtime: Operation | null; @@ -30,7 +31,8 @@ function snapshot(provider: ServerProvider): Snapshot { const connection = provider.connection?.operation; const update = provider.updateState; return { - source: runtime?.source ?? (provider.installed ? "unknown" : "missing"), + installed: provider.installed, + source: provider.installed ? (runtime?.source ?? "unknown") : "missing", state: provider.status, runtime: operation ? { @@ -115,6 +117,11 @@ function lifecycleEvent( /** Observes canonical snapshots without storing account, path, model, or message data. */ export function createProviderLifecycleAnalyticsMapper() { const previous = new Map(); + // Installation analytics is provider-level, not instance-level. A user may + // configure several instances of one driver, and exposing instance IDs would + // add unnecessary identity surface. "Installed" therefore means that at + // least one settled instance of the provider is installed. + const previousInstallations = new Map(); const observedStarts = new Map< string, Partial> @@ -124,7 +131,44 @@ export function createProviderLifecycleAnalyticsMapper() { const observe = (providers: ReadonlyArray): ReadonlyArray => { const events: Event[] = []; const present = new Set(); - for (const provider of providers.slice(0, MAX_INSTANCES)) { + const presentDrivers = new Set(); + const pendingDrivers = new Set(); + const installations = new Map(); + const limitedProviders = providers.slice(0, MAX_INSTANCES); + + for (const provider of limitedProviders) { + const driver = String(provider.driver); + presentDrivers.add(driver); + if (provider.probePending) { + pendingDrivers.add(driver); + continue; + } + installations.set(driver, (installations.get(driver) ?? false) || provider.installed); + } + + for (const [driver, installed] of installations) { + // Do not publish a provisional aggregate while any instance of the same + // provider is still being probed. + if (pendingDrivers.has(driver)) continue; + const before = previousInstallations.get(driver); + if (before === undefined) { + events.push({ + name: "provider.installation.observed", + properties: { provider: driver, installed }, + }); + } else if (before !== installed) { + events.push({ + name: "provider.installation.changed", + properties: { provider: driver, fromInstalled: before, toInstalled: installed }, + }); + } + previousInstallations.set(driver, installed); + } + + for (const driver of previousInstallations.keys()) + if (!presentDrivers.has(driver)) previousInstallations.delete(driver); + + for (const provider of limitedProviders) { const id = String(provider.instanceId); present.add(id); if (provider.probePending) continue; @@ -132,16 +176,7 @@ export function createProviderLifecycleAnalyticsMapper() { const next = snapshot(provider); previous.set(id, next); const driver = String(provider.driver); - if (!before) { - events.push({ - name: "provider.discovered", - properties: { - provider: driver, - source: next.source, - state: next.state, - }, - }); - } else { + if (before) { if (before.state !== next.state) events.push({ name: "provider.readiness.changed", @@ -181,6 +216,7 @@ export function createProviderLifecycleAnalyticsMapper() { observe, clear: () => { previous.clear(); + previousInstallations.clear(); observedStarts.clear(); initialized = false; }, diff --git a/docs/internals/product-analytics.md b/docs/internals/product-analytics.md index 5da283fe5..d620f32f4 100644 --- a/docs/internals/product-analytics.md +++ b/docs/internals/product-analytics.md @@ -61,7 +61,11 @@ protections below. Final copy and layout require human review before activation. `contract.ts` normalizes raw call-site values. `wireContract.ts` is the strict persisted/wire validator; the website gateway consumes its generated copy. -Revision 3 adds product insight signals while the envelope remains schema version 1. +Revision 3 added product insight signals. Revision 4 makes provider installation +state explicit, so a bundled-but-missing provider cannot be mistaken for an +installed runtime. Installation state is aggregated per provider driver: it is +true when at least one settled configured instance is installed. The envelope +remains schema version 1. Legacy events may omit `contractRevision`; new events carry the bounded revision. Unrecognized/custom model and build labels become safe categories, not raw text. @@ -71,8 +75,8 @@ registered event. Regenerate and compare both repositories from the desktop root ```sh node packages/scient-analytics/src/generateConformance.ts \ --wire=/absolute/website/workers/events/src/eventContract.ts \ - packages/scient-analytics/fixtures/contract-v3.json \ - /absolute/website/workers/events/fixtures/contract-v3.json + packages/scient-analytics/fixtures/contract-v4.json \ + /absolute/website/workers/events/fixtures/contract-v4.json # Repeat with --check to verify exact source/corpus parity without writing. ``` diff --git a/packages/scient-analytics/fixtures/contract-v4.json b/packages/scient-analytics/fixtures/contract-v4.json new file mode 100644 index 000000000..ed7cf7eb0 --- /dev/null +++ b/packages/scient-analytics/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/packages/scient-analytics/src/conformance.test.ts b/packages/scient-analytics/src/conformance.test.ts index 0e8e66c42..68a55f7e2 100644 --- a/packages/scient-analytics/src/conformance.test.ts +++ b/packages/scient-analytics/src/conformance.test.ts @@ -1,10 +1,23 @@ import { describe, expect, it } from "@effect/vitest"; -import fixture from "../fixtures/contract-v3.json" with { type: "json" }; +import legacy from "../fixtures/contract-v3.json" with { type: "json" }; +import fixture from "../fixtures/contract-v4.json" with { type: "json" }; import { buildAnalyticsConformanceFixture } from "./conformance.ts"; import { ANALYTICS_EVENT_NAMES, consentAllows, normalizeInheritedEvent } from "./contract.ts"; -import { eventContractViolation } from "./wireContract.ts"; +import { eventContractViolation, type PrivacyLevel } from "./wireContract.ts"; describe("analytics contract conformance corpus", () => { + it("continues accepting the complete revision 3 corpus", () => { + for (const entry of legacy.cases) + expect( + eventContractViolation({ + ...entry, + privacyLevel: entry.privacyLevel as PrivacyLevel, + consentLevel: entry.consentLevel as PrivacyLevel, + }), + entry.case, + ).toBeNull(); + }); + it("matches every registered normalizer exactly", () => { expect(buildAnalyticsConformanceFixture()).toEqual(fixture); expect(new Set(fixture.cases.map((entry) => entry.name)).size).toBe( diff --git a/packages/scient-analytics/src/conformance.ts b/packages/scient-analytics/src/conformance.ts index 985bdf227..a92a6a751 100644 --- a/packages/scient-analytics/src/conformance.ts +++ b/packages/scient-analytics/src/conformance.ts @@ -55,6 +55,9 @@ const representativeProperties = { value: "rtl", source: "scient_managed", state: "ready", + installed: true, + fromInstalled: false, + toInstalled: true, action: "repair", stage: "downloading", failureClass: "permission", diff --git a/packages/scient-analytics/src/contract.test.ts b/packages/scient-analytics/src/contract.test.ts index 2b96377d4..b7b6e2e18 100644 --- a/packages/scient-analytics/src/contract.test.ts +++ b/packages/scient-analytics/src/contract.test.ts @@ -115,7 +115,7 @@ describe("Scient analytics contract", () => { expect(surface?.properties).toEqual({ surface: "settings", ...context, - contractRevision: "3", + contractRevision: "4", }); }); diff --git a/packages/scient-analytics/src/contract.ts b/packages/scient-analytics/src/contract.ts index 7f9b45a5c..07fda445e 100644 --- a/packages/scient-analytics/src/contract.ts +++ b/packages/scient-analytics/src/contract.ts @@ -2,7 +2,7 @@ export const ANALYTICS_SCHEMA_VERSION = 1 as const; export const ANALYTICS_SOURCE = "desktop" as const; import { EVENT_DEFINITIONS } from "./wireContract.ts"; -export const ANALYTICS_CONTRACT_REVISION = "3" as const; +export const ANALYTICS_CONTRACT_REVISION = "4" as const; export const ANALYTICS_EVENT_NAMES = [ "app.session.started", @@ -22,7 +22,10 @@ export const ANALYTICS_EVENT_NAMES = [ "provider.turn.interrupted", "provider.request.responded", "provider.conversation.rolled_back", + // Legacy producer event retained in the wire contract for older releases. "provider.discovered", + "provider.installation.observed", + "provider.installation.changed", "provider.readiness.changed", "provider.runtime.source.changed", "provider.lifecycle.started", @@ -499,6 +502,27 @@ function normalizeEvent( state: normalizedEnum(property(input, "state"), PROVIDER_STATES), }, }; + case "provider.installation.observed": + return { + name, + privacyLevel: "product", + priority: "summary", + properties: { + provider, + installed: normalizedBoolean(property(input, "installed")), + }, + }; + case "provider.installation.changed": + return { + name, + privacyLevel: "product", + priority: "core", + properties: { + provider, + fromInstalled: normalizedBoolean(property(input, "fromInstalled")), + toInstalled: normalizedBoolean(property(input, "toInstalled")), + }, + }; case "provider.readiness.changed": return { name, diff --git a/packages/scient-analytics/src/generateConformance.ts b/packages/scient-analytics/src/generateConformance.ts index 7e8e4d73e..601e3e0c7 100644 --- a/packages/scient-analytics/src/generateConformance.ts +++ b/packages/scient-analytics/src/generateConformance.ts @@ -5,7 +5,7 @@ import * as NodeURL from "node:url"; import { buildAnalyticsConformanceFixture } from "./conformance.ts"; -const defaultPath = NodeURL.fileURLToPath(new URL("../fixtures/contract-v3.json", import.meta.url)); +const defaultPath = NodeURL.fileURLToPath(new URL("../fixtures/contract-v4.json", import.meta.url)); const args = process.argv.slice(2); const check = args.includes("--check"); const wireArgument = args.find((arg) => arg.startsWith("--wire=")); diff --git a/packages/scient-analytics/src/wireContract.ts b/packages/scient-analytics/src/wireContract.ts index 8cefee379..c13566c86 100644 --- a/packages/scient-analytics/src/wireContract.ts +++ b/packages/scient-analytics/src/wireContract.ts @@ -214,6 +214,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 }, @@ -657,7 +669,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)) {