diff --git a/packages/shared/src/telemetry/events.ts b/packages/shared/src/telemetry/events.ts index 6b30995ed22..fe07152c87a 100644 --- a/packages/shared/src/telemetry/events.ts +++ b/packages/shared/src/telemetry/events.ts @@ -50,9 +50,12 @@ export function trackGoalCreated( export function trackAgentCreated( client: TelemetryClient, - dims: { agentRole: string }, + dims: { agentRole: string; agentId: string }, ): void { - client.track("agent.created", { agent_role: dims.agentRole }); + client.track("agent.created", { + agent_role: dims.agentRole, + agent_id: dims.agentId, + }); } export function trackSkillImported( @@ -67,16 +70,22 @@ export function trackSkillImported( export function trackAgentFirstHeartbeat( client: TelemetryClient, - dims: { agentRole: string }, + dims: { agentRole: string; agentId: string }, ): void { - client.track("agent.first_heartbeat", { agent_role: dims.agentRole }); + client.track("agent.first_heartbeat", { + agent_role: dims.agentRole, + agent_id: dims.agentId, + }); } export function trackAgentTaskCompleted( client: TelemetryClient, - dims: { agentRole: string }, + dims: { agentRole: string; agentId: string }, ): void { - client.track("agent.task_completed", { agent_role: dims.agentRole }); + client.track("agent.task_completed", { + agent_role: dims.agentRole, + agent_id: dims.agentId, + }); } export function trackErrorHandlerCrash( diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 91a7dbffcd6..d3b98166251 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -352,6 +352,7 @@ describe("agent skill routes", () => { ); expect(mockTrackAgentCreated).toHaveBeenCalledWith(expect.anything(), { agentRole: "engineer", + agentId: "11111111-1111-4111-8111-111111111111", }); }); diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index 77538331b8a..31a9cc59bdf 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -273,7 +273,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); it("tracks the first heartbeat with the agent role instead of adapter type", async () => { - const { runId } = await seedRunFixture({ + const { agentId, runId } = await seedRunFixture({ agentStatus: "running", includeIssue: false, }); @@ -283,6 +283,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(mockTrackAgentFirstHeartbeat).toHaveBeenCalledWith(mockTelemetryClient, { agentRole: "engineer", + agentId, }); }); }); diff --git a/server/src/__tests__/issue-telemetry-routes.test.ts b/server/src/__tests__/issue-telemetry-routes.test.ts index 80d8641b6ff..1b0196d60be 100644 --- a/server/src/__tests__/issue-telemetry-routes.test.ts +++ b/server/src/__tests__/issue-telemetry-routes.test.ts @@ -118,6 +118,7 @@ describe("issue telemetry routes", () => { await vi.waitFor(() => { expect(mockTrackAgentTaskCompleted).toHaveBeenCalledWith(expect.anything(), { agentRole: "engineer", + agentId: "agent-1", }); }); }, 10_000); diff --git a/server/src/__tests__/shared-telemetry-events.test.ts b/server/src/__tests__/shared-telemetry-events.test.ts new file mode 100644 index 00000000000..1d6483d2e88 --- /dev/null +++ b/server/src/__tests__/shared-telemetry-events.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from "vitest"; +import { + trackAgentCreated, + trackAgentFirstHeartbeat, + trackAgentTaskCompleted, + trackInstallCompleted, +} from "@paperclipai/shared/telemetry"; +import type { TelemetryClient } from "@paperclipai/shared/telemetry"; + +function createClient(): TelemetryClient { + return { + track: vi.fn(), + hashPrivateRef: vi.fn((value: string) => `hashed:${value}`), + } as unknown as TelemetryClient; +} + +describe("shared telemetry agent events", () => { + it("includes agent_id for agent.created", () => { + const client = createClient(); + + trackAgentCreated(client, { + agentRole: "engineer", + agentId: "11111111-1111-4111-8111-111111111111", + }); + + expect(client.track).toHaveBeenCalledWith("agent.created", { + agent_role: "engineer", + agent_id: "11111111-1111-4111-8111-111111111111", + }); + }); + + it("includes agent_id for agent.first_heartbeat", () => { + const client = createClient(); + + trackAgentFirstHeartbeat(client, { + agentRole: "coder", + agentId: "22222222-2222-4222-8222-222222222222", + }); + + expect(client.track).toHaveBeenCalledWith("agent.first_heartbeat", { + agent_role: "coder", + agent_id: "22222222-2222-4222-8222-222222222222", + }); + }); + + it("includes agent_id for agent.task_completed", () => { + const client = createClient(); + + trackAgentTaskCompleted(client, { + agentRole: "qa", + agentId: "33333333-3333-4333-8333-333333333333", + }); + + expect(client.track).toHaveBeenCalledWith("agent.task_completed", { + agent_role: "qa", + agent_id: "33333333-3333-4333-8333-333333333333", + }); + }); + + it("keeps non-agent event dimensions unchanged", () => { + const client = createClient(); + + trackInstallCompleted(client, { adapterType: "codex_local" }); + + expect(client.track).toHaveBeenCalledWith("install.completed", { + adapter_type: "codex_local", + }); + expect(client.track).not.toHaveBeenCalledWith( + "install.completed", + expect.objectContaining({ agent_id: expect.any(String) }), + ); + }); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 266803ecd9b..39a79dd6e6e 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1426,7 +1426,7 @@ export function agentRoutes(db: Db) { }); const telemetryClient = getTelemetryClient(); if (telemetryClient) { - trackAgentCreated(telemetryClient, { agentRole: agent.role }); + trackAgentCreated(telemetryClient, { agentRole: agent.role, agentId: agent.id }); } await applyDefaultAgentTaskAssignGrant( @@ -1514,7 +1514,7 @@ export function agentRoutes(db: Db) { }); const telemetryClient = getTelemetryClient(); if (telemetryClient) { - trackAgentCreated(telemetryClient, { agentRole: agent.role }); + trackAgentCreated(telemetryClient, { agentRole: agent.role, agentId: agent.id }); } await applyDefaultAgentTaskAssignGrant( diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 344c262c738..4a72bcd9062 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -1661,7 +1661,7 @@ export function issueRoutes( if (tc && actor.agentId) { const actorAgent = await agentsSvc.getById(actor.agentId); if (actorAgent) { - trackAgentTaskCompleted(tc, { agentRole: actorAgent.role }); + trackAgentTaskCompleted(tc, { agentRole: actorAgent.role, agentId: actorAgent.id }); } } } diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index d94922a0f76..bc672c77a78 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -2300,7 +2300,7 @@ export function heartbeatService(db: Db) { if (isFirstHeartbeat && updated) { const tc = getTelemetryClient(); - if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.role }); + if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.role, agentId: updated.id }); } if (updated) {