Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions apps/server/src/telemetry/ProviderLifecycleAnalytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
]);
});

Expand All @@ -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" },
Expand All @@ -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 },
});
});
});
60 changes: 48 additions & 12 deletions apps/server/src/telemetry/ProviderLifecycleAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Operation {
}

interface Snapshot {
readonly installed: boolean;
readonly source: string;
readonly state: string;
readonly runtime: Operation | null;
Expand All @@ -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
? {
Expand Down Expand Up @@ -115,6 +117,11 @@ function lifecycleEvent(
/** Observes canonical snapshots without storing account, path, model, or message data. */
export function createProviderLifecycleAnalyticsMapper() {
const previous = new Map<string, Snapshot>();
// 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<string, boolean>();
const observedStarts = new Map<
string,
Partial<Record<"runtime" | "connection" | "update", string>>
Expand All @@ -124,24 +131,52 @@ export function createProviderLifecycleAnalyticsMapper() {
const observe = (providers: ReadonlyArray<ServerProvider>): ReadonlyArray<Event> => {
const events: Event[] = [];
const present = new Set<string>();
for (const provider of providers.slice(0, MAX_INSTANCES)) {
const presentDrivers = new Set<string>();
const pendingDrivers = new Set<string>();
const installations = new Map<string, boolean>();
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;
const before = previous.get(id);
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",
Expand Down Expand Up @@ -181,6 +216,7 @@ export function createProviderLifecycleAnalyticsMapper() {
observe,
clear: () => {
previous.clear();
previousInstallations.clear();
observedStarts.clear();
initialized = false;
},
Expand Down
10 changes: 7 additions & 3 deletions docs/internals/product-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
```

Expand Down
Loading
Loading