diff --git a/packages/shared/src/telemetry/events.ts b/packages/shared/src/telemetry/events.ts index 6b30995ed22..1b347f3cd1f 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, + ...(dims.agentId ? { agent_id: dims.agentId } : {}), + }); } export function trackSkillImported( @@ -67,16 +70,24 @@ 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, + ...(dims.agentId ? { agent_id: dims.agentId } : {}), + }); } export function trackAgentTaskCompleted( client: TelemetryClient, - dims: { agentRole: string }, + dims: { agentRole: string; agentId?: string; adapterType?: string; model?: string }, ): void { - client.track("agent.task_completed", { agent_role: dims.agentRole }); + client.track("agent.task_completed", { + agent_role: dims.agentRole, + ...(dims.agentId ? { agent_id: dims.agentId } : {}), + ...(dims.adapterType ? { adapter_type: dims.adapterType } : {}), + ...(dims.model ? { model: dims.model } : {}), + }); } export function trackErrorHandlerCrash( diff --git a/server/src/__tests__/issue-telemetry-routes.test.ts b/server/src/__tests__/issue-telemetry-routes.test.ts index 0451f9f1253..bd2e1f31485 100644 --- a/server/src/__tests__/issue-telemetry-routes.test.ts +++ b/server/src/__tests__/issue-telemetry-routes.test.ts @@ -1,8 +1,6 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { errorHandler } from "../middleware/index.js"; -import { issueRoutes } from "../routes/issues.js"; const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), @@ -18,39 +16,41 @@ const mockAgentService = vi.hoisted(() => ({ const mockTrackAgentTaskCompleted = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); -vi.mock("@paperclipai/shared/telemetry", () => ({ - trackAgentTaskCompleted: mockTrackAgentTaskCompleted, - trackErrorHandlerCrash: vi.fn(), -})); +function registerModuleMocks() { + vi.doMock("@paperclipai/shared/telemetry", () => ({ + trackAgentTaskCompleted: mockTrackAgentTaskCompleted, + trackErrorHandlerCrash: vi.fn(), + })); -vi.mock("../telemetry.js", () => ({ - getTelemetryClient: mockGetTelemetryClient, -})); + vi.doMock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, + })); -vi.mock("../services/index.js", () => ({ - accessService: () => ({ - canUser: vi.fn(), - hasPermission: vi.fn(), - }), - agentService: () => mockAgentService, - documentService: () => ({}), - executionWorkspaceService: () => ({}), - feedbackService: () => ({}), - goalService: () => ({}), - heartbeatService: () => ({ - wakeup: vi.fn(async () => undefined), - reportRunActivity: vi.fn(async () => undefined), - }), - instanceSettingsService: () => ({}), - issueApprovalService: () => ({}), - issueService: () => mockIssueService, - logActivity: vi.fn(async () => undefined), - projectService: () => ({}), - routineService: () => ({ - syncRunStatusForIssue: vi.fn(async () => undefined), - }), - workProductService: () => ({}), -})); + vi.doMock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(), + hasPermission: vi.fn(), + }), + agentService: () => mockAgentService, + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => ({}), + goalService: () => ({}), + heartbeatService: () => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + }), + instanceSettingsService: () => ({}), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: vi.fn(async () => undefined), + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), + })); +} function makeIssue(status: "todo" | "done") { return { @@ -65,7 +65,11 @@ function makeIssue(status: "todo" | "done") { }; } -function createApp(actor: Record) { +async function createApp(actor: Record) { + const [{ errorHandler }, { issueRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/issues.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -79,7 +83,15 @@ function createApp(actor: Record) { describe("issue telemetry routes", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetModules(); + vi.doUnmock("@paperclipai/shared/telemetry"); + vi.doUnmock("../telemetry.js"); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockIssueService.getById.mockResolvedValue(makeIssue("todo")); mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); @@ -90,15 +102,16 @@ describe("issue telemetry routes", () => { })); }); - it("emits task-completed telemetry with the agent role", async () => { + it("emits task-completed telemetry with the agent role, adapter type, and model", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", adapterType: "codex_local", + adapterConfig: { model: "claude-sonnet-4-6" }, }); - const app = createApp({ + const app = await createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", @@ -112,12 +125,15 @@ describe("issue telemetry routes", () => { await vi.waitFor(() => { expect(mockTrackAgentTaskCompleted).toHaveBeenCalledWith(expect.anything(), { agentRole: "engineer", + agentId: "agent-1", + adapterType: "codex_local", + model: "claude-sonnet-4-6", }); }); }, 10_000); it("does not emit agent task-completed telemetry for board-driven completions", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 59df4536235..a3da3cec97c 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -1693,7 +1693,13 @@ export function issueRoutes( if (tc && actor.agentId) { const actorAgent = await agentsSvc.getById(actor.agentId); if (actorAgent) { - trackAgentTaskCompleted(tc, { agentRole: actorAgent.role }); + const model = typeof actorAgent.adapterConfig?.model === "string" ? actorAgent.adapterConfig.model : undefined; + trackAgentTaskCompleted(tc, { + agentRole: actorAgent.role, + agentId: actorAgent.id, + adapterType: actorAgent.adapterType, + model, + }); } } }