mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
feat(telemetry): emit adapter_type and model on agent.task_completed
The telemetry backend already retains these dimensions (d36ddc1) but the app was not sending them. Now trackAgentTaskCompleted forwards adapterType from the agent record and model from adapterConfig when available, so completions-per-model and completions-per-adapter charts will populate as data flows in. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1ab9cd8dfe
commit
5a698b0446
3 files changed
+78
-45
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
async function createApp(actor: Record<string, unknown>) {
|
||||
const [{ errorHandler }, { issueRoutes }] = await Promise.all([
|
||||
vi.importActual<typeof import("../middleware/index.js")>("../middleware/index.js"),
|
||||
vi.importActual<typeof import("../routes/issues.js")>("../routes/issues.js"),
|
||||
]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
@@ -79,7 +83,15 @@ function createApp(actor: Record<string, unknown>) {
|
||||
|
||||
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"],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user