fix: align telemetry client payload and dimensions with backend schema
Restructure the TelemetryClient to send the correct backend envelope
format ({app, schemaVersion, installId, events: [{name, occurredAt, dimensions}]})
instead of the old per-event format. Update all event dimension names
to match the backend registry (agent_role, adapter_type, error_code, etc.).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
f16de6026d
commit
53dbcd185e
9 changed files with 49 additions and 48 deletions
|
|
@ -359,7 +359,7 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
|||
}
|
||||
|
||||
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<void> {
|
|||
writeConfig(config, opts.config);
|
||||
|
||||
if (tc) trackInstallCompleted(tc, {
|
||||
setupMode,
|
||||
dbMode: database.mode,
|
||||
deploymentMode: server.deploymentMode,
|
||||
adapterType: server.deploymentMode,
|
||||
});
|
||||
|
||||
p.note(
|
||||
|
|
|
|||
|
|
@ -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<typeof setInterval> | 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<string, string | number | boolean>): 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 {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export {
|
|||
export type {
|
||||
TelemetryConfig,
|
||||
TelemetryState,
|
||||
TelemetryEvent,
|
||||
TelemetryEventEnvelope,
|
||||
TelemetryEventName,
|
||||
} from "./types.js";
|
||||
|
|
|
|||
|
|
@ -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<string, string | number | boolean>;
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ describe("issue telemetry routes", () => {
|
|||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockTrackAgentTaskCompleted).toHaveBeenCalledWith(expect.anything(), {
|
||||
adapterType: "codex_local",
|
||||
agentRole: "codex_local",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue