diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index cd7b6fe88b..c6bc08c899 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -359,7 +359,7 @@ export async function onboard(opts: OnboardOptions): Promise { } const tc = getTelemetryClient(); - if (tc) trackInstallStarted(tc, { setupMode }); + if (tc) trackInstallStarted(tc); let llm: PaperclipConfig["llm"] | undefined; const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv(); @@ -510,9 +510,7 @@ export async function onboard(opts: OnboardOptions): Promise { writeConfig(config, opts.config); if (tc) trackInstallCompleted(tc, { - setupMode, - dbMode: database.mode, - deploymentMode: server.deploymentMode, + adapterType: server.deploymentMode, }); p.note( diff --git a/packages/shared/src/telemetry/client.ts b/packages/shared/src/telemetry/client.ts index e4503c4fde..939a32ed05 100644 --- a/packages/shared/src/telemetry/client.ts +++ b/packages/shared/src/telemetry/client.ts @@ -1,8 +1,7 @@ -import os from "node:os"; import { createHash } from "node:crypto"; import type { TelemetryConfig, - TelemetryEventEnvelope, + TelemetryEvent, TelemetryEventName, TelemetryState, } from "./types.js"; @@ -12,11 +11,10 @@ const BATCH_SIZE = 50; const SEND_TIMEOUT_MS = 5_000; export class TelemetryClient { - private queue: TelemetryEventEnvelope[] = []; + private queue: TelemetryEvent[] = []; private readonly config: TelemetryConfig; private readonly stateFactory: () => TelemetryState; private readonly version: string; - private readonly sessionId: string; private state: TelemetryState | null = null; private flushInterval: ReturnType | null = null; @@ -24,22 +22,16 @@ export class TelemetryClient { this.config = config; this.stateFactory = stateFactory; this.version = version; - this.sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } track(eventName: TelemetryEventName, dimensions?: Record): void { if (!this.config.enabled) return; - const state = this.getState(); + this.getState(); // ensure state is initialised (side-effect: creates state file on first call) this.queue.push({ - installId: state.installId, - sessionId: this.sessionId, - event: eventName, + name: eventName, + occurredAt: new Date().toISOString(), dimensions: dimensions ?? {}, - timestamp: new Date().toISOString(), - version: this.version, - os: os.platform(), - arch: os.arch(), }); if (this.queue.length >= BATCH_SIZE) { @@ -51,7 +43,10 @@ export class TelemetryClient { if (!this.config.enabled || this.queue.length === 0) return; const events = this.queue.splice(0); + const state = this.getState(); const endpoint = this.config.endpoint ?? DEFAULT_ENDPOINT; + const app = this.config.app ?? "paperclip"; + const schemaVersion = this.config.schemaVersion ?? "1"; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS); @@ -59,7 +54,12 @@ export class TelemetryClient { await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ events }), + body: JSON.stringify({ + app, + schemaVersion, + installId: state.installId, + events, + }), signal: controller.signal, }); } catch { diff --git a/packages/shared/src/telemetry/events.ts b/packages/shared/src/telemetry/events.ts index f44f7908a5..1ed96bb6a6 100644 --- a/packages/shared/src/telemetry/events.ts +++ b/packages/shared/src/telemetry/events.ts @@ -1,14 +1,14 @@ import type { TelemetryClient } from "./client.js"; -export function trackInstallStarted(client: TelemetryClient, dims: { setupMode: string }): void { - client.track("install.started", dims); +export function trackInstallStarted(client: TelemetryClient): void { + client.track("install.started"); } export function trackInstallCompleted( client: TelemetryClient, - dims: { setupMode: string; dbMode: string; deploymentMode: string }, + dims: { adapterType: string }, ): void { - client.track("install.completed", dims); + client.track("install.completed", { adapter_type: dims.adapterType }); } export function trackCompanyImported( @@ -17,33 +17,29 @@ export function trackCompanyImported( ): void { const ref = dims.isPrivate ? client.hashPrivateRef(dims.sourceRef) : dims.sourceRef; client.track("company.imported", { - sourceType: dims.sourceType, - sourceRef: ref, - sourceRefHashed: dims.isPrivate, + source_type: dims.sourceType, + source_ref: ref, + source_ref_hashed: dims.isPrivate, }); } export function trackAgentFirstHeartbeat( client: TelemetryClient, - dims: { adapterType: string }, + dims: { agentRole: string }, ): void { - client.track("agent.first_heartbeat", dims); + client.track("agent.first_heartbeat", { agent_role: dims.agentRole }); } export function trackAgentTaskCompleted( client: TelemetryClient, - dims: { adapterType: string }, + dims: { agentRole: string }, ): void { - client.track("agent.task_completed", dims); + client.track("agent.task_completed", { agent_role: dims.agentRole }); } export function trackErrorHandlerCrash( client: TelemetryClient, - dims: { errorName: string; route: string; method: string }, + dims: { errorCode: string }, ): void { - client.track("error.handler_crash", { - errorName: dims.errorName, - route: dims.route, - method: dims.method, - }); + client.track("error.handler_crash", { error_code: dims.errorCode }); } diff --git a/packages/shared/src/telemetry/index.ts b/packages/shared/src/telemetry/index.ts index 520a56af90..1757276ec4 100644 --- a/packages/shared/src/telemetry/index.ts +++ b/packages/shared/src/telemetry/index.ts @@ -12,6 +12,7 @@ export { export type { TelemetryConfig, TelemetryState, + TelemetryEvent, TelemetryEventEnvelope, TelemetryEventName, } from "./types.js"; diff --git a/packages/shared/src/telemetry/types.ts b/packages/shared/src/telemetry/types.ts index f5df091495..cb0fed735f 100644 --- a/packages/shared/src/telemetry/types.ts +++ b/packages/shared/src/telemetry/types.ts @@ -8,17 +8,23 @@ export interface TelemetryState { export interface TelemetryConfig { enabled: boolean; endpoint?: string; + app?: string; + schemaVersion?: string; } -export interface TelemetryEventEnvelope { - installId: string; - sessionId: string; - event: string; +/** Per-event object inside the backend envelope */ +export interface TelemetryEvent { + name: string; + occurredAt: string; dimensions: Record; - timestamp: string; - version: string; - os: string; - arch: string; +} + +/** Full payload sent to the backend ingest endpoint */ +export interface TelemetryEventEnvelope { + app: string; + schemaVersion: string; + installId: string; + events: TelemetryEvent[]; } export type TelemetryEventName = diff --git a/server/src/__tests__/issue-telemetry-routes.test.ts b/server/src/__tests__/issue-telemetry-routes.test.ts index 9e6b5fb6dd..ff51f722de 100644 --- a/server/src/__tests__/issue-telemetry-routes.test.ts +++ b/server/src/__tests__/issue-telemetry-routes.test.ts @@ -100,7 +100,7 @@ describe("issue telemetry routes", () => { expect(res.status).toBe(200); expect(mockTrackAgentTaskCompleted).toHaveBeenCalledWith(expect.anything(), { - adapterType: "codex_local", + agentRole: "codex_local", }); }); diff --git a/server/src/middleware/error-handler.ts b/server/src/middleware/error-handler.ts index d68789ef31..032b5e1f19 100644 --- a/server/src/middleware/error-handler.ts +++ b/server/src/middleware/error-handler.ts @@ -47,7 +47,7 @@ export function errorHandler( err, ); const tc = getTelemetryClient(); - if (tc) trackErrorHandlerCrash(tc, { errorName: err.name, route: req.route?.path ?? req.path, method: req.method }); + if (tc) trackErrorHandlerCrash(tc, { errorCode: err.name }); } res.status(err.status).json({ error: err.message, @@ -72,7 +72,7 @@ export function errorHandler( ); const tc = getTelemetryClient(); - if (tc) trackErrorHandlerCrash(tc, { errorName: rootError.name, route: req.route?.path ?? req.path, method: req.method }); + if (tc) trackErrorHandlerCrash(tc, { errorCode: rootError.name }); res.status(500).json({ error: "Internal server error" }); } diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 7c031c8b13..e815f8aec3 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -1184,7 +1184,7 @@ export function issueRoutes(db: Db, storage: StorageService) { if (tc && actor.agentId) { const actorAgent = await agentsSvc.getById(actor.agentId); if (actorAgent) { - trackAgentTaskCompleted(tc, { adapterType: actorAgent.adapterType }); + trackAgentTaskCompleted(tc, { agentRole: actorAgent.adapterType }); } } } diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index e64c216c9f..507bf66396 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1832,7 +1832,7 @@ export function heartbeatService(db: Db) { if (isFirstHeartbeat && updated) { const tc = getTelemetryClient(); - if (tc) trackAgentFirstHeartbeat(tc, { adapterType: updated.adapterType }); + if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.adapterType }); } if (updated) {