feat(telemetry): carry app_version on instance_started (#681)

app_version was attached only to feedback events; every other allowlist stripped it, so the install base could not be segmented by release. One property on the once-per-boot census event covers it. Fixes #674.
This commit is contained in:
SnapOtter
2026-07-30 09:40:53 +08:00
committed by GitHub
parent fe21f352f6
commit b6c69b7aeb
6 changed files with 15 additions and 3 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ Emitted from `apps/api` through `trackEvent()`; properties are filtered by `anal
| Event | Fires when | Key properties |
| --- | --- | --- |
| `instance_started` | Once per boot | `arch`, `os_platform`, `deploy_mode`, `gpu_present` |
| `instance_started` | Once per boot | `arch`, `os_platform`, `deploy_mode`, `gpu_present`, `app_version` |
| `auth_login` | A login succeeds | `method` (`password` or `oidc`) |
| `auth_login_failed` | A login attempt fails | `method` (`password` or `oidc`) |
| `tool_used` | A tool job finishes | `tool_id`, `status`, `duration_ms`, `category`, `is_ai_tool`, `is_batch`, `input_format`, `output_format`, `bytes_in`, `bytes_out`, `execution_hint`, `error_code`, `error_kind` |
+1 -1
View File
@@ -26,7 +26,7 @@ const ALLOWED: Record<string, ReadonlySet<string>> = {
"status",
]),
ai_bundle_action: new Set(["bundle_id", "action", "duration_ms"]),
instance_started: new Set(["arch", "os_platform", "deploy_mode", "gpu_present"]),
instance_started: new Set(["arch", "os_platform", "deploy_mode", "gpu_present", "app_version"]),
auth_login: new Set(["method"]),
auth_login_failed: new Set(["method"]),
};
+2 -1
View File
@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import os from "node:os";
import type { InstanceStartedProperties } from "@snapotter/shared";
import { APP_VERSION, type InstanceStartedProperties } from "@snapotter/shared";
import { deployMode } from "./deploy-mode.js";
// Facts about the running instance, shipped once at boot as the
@@ -19,5 +19,6 @@ export function gatherSystemProperties(): InstanceStartedProperties {
os_platform: os.platform(),
deploy_mode: deployMode(),
gpu_present: existsSync("/dev/nvidia0"),
app_version: APP_VERSION,
};
}
+2
View File
@@ -79,6 +79,8 @@ export interface InstanceStartedProperties {
os_platform: string;
deploy_mode: "embedded" | "external" | "native";
gpu_present: boolean;
/** APP_VERSION at boot; the only event property that segments the install base by release. */
app_version: string;
}
export interface EditorToolUsedProperties {
@@ -59,6 +59,7 @@ describe("sanitizeEventProperties", () => {
os_platform: "linux",
deploy_mode: "embedded",
gpu_present: false,
app_version: "2.2.0",
hostname: "leaked-hostname",
});
expect(out).toEqual({
@@ -66,6 +67,7 @@ describe("sanitizeEventProperties", () => {
os_platform: "linux",
deploy_mode: "embedded",
gpu_present: false,
app_version: "2.2.0",
});
expect(out).not.toHaveProperty("hostname");
});
+7
View File
@@ -10,6 +10,7 @@ vi.mock("../../../apps/api/src/lib/deploy-mode.js", () => ({
deployMode: mockDeployMode,
}));
import { APP_VERSION } from "@snapotter/shared";
import { gatherSystemProperties } from "../../../apps/api/src/lib/system-info.js";
describe("gatherSystemProperties", () => {
@@ -47,6 +48,12 @@ describe("gatherSystemProperties", () => {
expect(gatherSystemProperties().gpu_present).toBe(false);
});
it("carries the app version so release adoption is measurable", () => {
mockDeployMode.mockReturnValue("embedded");
mockExistsSync.mockReturnValue(false);
expect(gatherSystemProperties().app_version).toBe(APP_VERSION);
});
it("passes through deploy mode and os platform", () => {
mockDeployMode.mockReturnValue("embedded");
mockExistsSync.mockReturnValue(false);