Skip to content

Commit fcfe6bc

Browse files
authored
fix(runtime): Restore telemetry in v2 endpoints after refactor (CopilotKit#4266)
<!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. **Please PLEASE reach out to us first before starting any significant work on new or existing features.** By the time you've gotten here, you're looking at creating a pull request so hopefully we're not too late. We love community contributions! That said, we want to make sure we're all on the same page before you start. Investing a lot of time and effort just to find out it doesn't align with the upstream project feels awful, and we don't want that to happen. It also helps to make sure the work you're planning isn't already in progress. As described in our contributing guide, please file an issue first: https://github.com/ag-ui-protocol/ag-ui/issues Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D You can learn more about contributing to copilotkit here: https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md Happy contributing! --> ## What does this PR do? (Describe the changes introduced in this PR) This PR restores several telemetry tracking calls that were inadvertently removed during the migration to have all endpoints use 'fetch'. It doesn't affect functionality, only metrics gathering. Segment publishing is removed because nothing in v2 telemetry downstream uses anything via Segment. Test coverage is intentionally a bit elevated for this, as metrics silently going away affects downstream flows and is effectively an integration concern more than a unit test one. ## Related PRs and Issues - (Direct link to related PR or issue, if relevant) ## Checklist - [ ] I have read the [Contribution Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md) - [ ] If the PR changes or adds functionality, I have updated the relevant documentation
2 parents 288d359 + b8e7b57 commit fcfe6bc

14 files changed

Lines changed: 879 additions & 133 deletions
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Integration test: Express single-route adapter (deprecated convenience
3+
* wrapper) + telemetry. This adapter delegates to createCopilotExpressHandler
4+
* with mode: "single-route".
5+
*/
6+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
7+
import type { AbstractAgent } from "@ag-ui/client";
8+
import { Observable, of } from "rxjs";
9+
10+
import { telemetry } from "../telemetry";
11+
import { createCopilotEndpointSingleRouteExpress } from "../endpoints/express-single";
12+
import { CopilotRuntime } from "../core/runtime";
13+
14+
function makeAgent(): AbstractAgent {
15+
const a: unknown = { execute: async () => ({ events: [] }) };
16+
(a as { clone: () => unknown }).clone = () => makeAgent();
17+
return a as AbstractAgent;
18+
}
19+
20+
function makeRuntime() {
21+
const runner = {
22+
run: () =>
23+
new Observable((observer) => {
24+
observer.next({});
25+
observer.complete();
26+
return () => undefined;
27+
}),
28+
connect: () => of({}),
29+
stop: async () => true,
30+
};
31+
return new CopilotRuntime({
32+
agents: { default: makeAgent() },
33+
runner,
34+
});
35+
}
36+
37+
describe("Express single-route adapter — telemetry firing (integration)", () => {
38+
let captureSpy: ReturnType<typeof vi.spyOn>;
39+
40+
beforeEach(() => {
41+
captureSpy = vi.spyOn(telemetry, "capture").mockResolvedValue(undefined);
42+
});
43+
44+
afterEach(() => {
45+
captureSpy.mockRestore();
46+
});
47+
48+
it("fires instance_created on handler creation", async () => {
49+
const runtime = makeRuntime();
50+
createCopilotEndpointSingleRouteExpress({
51+
runtime,
52+
basePath: "/api/copilotkit",
53+
});
54+
55+
await vi.waitFor(() => {
56+
expect(captureSpy).toHaveBeenCalledWith(
57+
"oss.runtime.instance_created",
58+
expect.objectContaining({
59+
agentsAmount: 1,
60+
"cloud.api_key_provided": false,
61+
}),
62+
);
63+
});
64+
});
65+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Integration test: Express adapter + telemetry.
3+
*/
4+
import express from "express";
5+
import request from "supertest";
6+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
7+
import type { AbstractAgent } from "@ag-ui/client";
8+
import { Observable, of } from "rxjs";
9+
10+
import { telemetry } from "../telemetry";
11+
import { createCopilotExpressHandler } from "../endpoints/express";
12+
import { CopilotRuntime } from "../core/runtime";
13+
14+
function makeAgent(): AbstractAgent {
15+
const a: unknown = { execute: async () => ({ events: [] }) };
16+
(a as { clone: () => unknown }).clone = () => makeAgent();
17+
return a as AbstractAgent;
18+
}
19+
20+
function makeRuntime() {
21+
const runner = {
22+
run: () =>
23+
new Observable((observer) => {
24+
observer.next({});
25+
observer.complete();
26+
return () => undefined;
27+
}),
28+
connect: () => of({}),
29+
stop: async () => true,
30+
};
31+
return new CopilotRuntime({
32+
agents: { default: makeAgent() },
33+
runner,
34+
});
35+
}
36+
37+
describe("Express adapter — telemetry firing (integration)", () => {
38+
let captureSpy: ReturnType<typeof vi.spyOn>;
39+
40+
beforeEach(() => {
41+
captureSpy = vi.spyOn(telemetry, "capture").mockResolvedValue(undefined);
42+
});
43+
44+
afterEach(() => {
45+
captureSpy.mockRestore();
46+
});
47+
48+
it("fires instance_created on handler creation (multi-route)", async () => {
49+
const runtime = makeRuntime();
50+
createCopilotExpressHandler({ runtime, basePath: "/" });
51+
52+
await vi.waitFor(() => {
53+
expect(captureSpy).toHaveBeenCalledWith(
54+
"oss.runtime.instance_created",
55+
expect.objectContaining({
56+
agentsAmount: 1,
57+
"cloud.api_key_provided": false,
58+
}),
59+
);
60+
});
61+
});
62+
63+
it("fires copilot_request_created when a real HTTP request hits the handler", async () => {
64+
const runtime = makeRuntime();
65+
const app = express();
66+
app.use(createCopilotExpressHandler({ runtime, basePath: "/" }));
67+
68+
await request(app)
69+
.post("/agent/default/run")
70+
.set("Content-Type", "application/json")
71+
.send({ messages: [], state: {}, threadId: "t1" });
72+
73+
expect(captureSpy).toHaveBeenCalledWith(
74+
"oss.runtime.copilot_request_created",
75+
expect.objectContaining({
76+
requestType: "run",
77+
"cloud.api_key_provided": false,
78+
}),
79+
);
80+
});
81+
82+
it("includes cloud.public_api_key on request when header is present", async () => {
83+
const runtime = makeRuntime();
84+
const app = express();
85+
app.use(createCopilotExpressHandler({ runtime, basePath: "/" }));
86+
87+
await request(app)
88+
.post("/agent/default/run")
89+
.set("Content-Type", "application/json")
90+
.set("x-copilotcloud-public-api-key", "ck_pub_test_xyz")
91+
.send({ messages: [], state: {}, threadId: "t1" });
92+
93+
expect(captureSpy).toHaveBeenCalledWith(
94+
"oss.runtime.copilot_request_created",
95+
expect.objectContaining({
96+
"cloud.api_key_provided": true,
97+
"cloud.public_api_key": "ck_pub_test_xyz",
98+
}),
99+
);
100+
});
101+
});

packages/runtime/src/v2/runtime/__tests__/handle-connect.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,68 @@ describe("handleConnectAgent", () => {
475475
}
476476
});
477477
});
478+
479+
describe("telemetry", () => {
480+
it("captures oss.runtime.copilot_request_created on every invocation", async () => {
481+
const { telemetry } = await import("../telemetry");
482+
const captureSpy = vi
483+
.spyOn(telemetry, "capture")
484+
.mockResolvedValue(undefined);
485+
486+
try {
487+
const runtime = createMockRuntime({});
488+
const request = new Request("https://example.com/agent/test/connect", {
489+
method: "POST",
490+
});
491+
await handleConnectAgent({
492+
runtime,
493+
request,
494+
agentId: "nonexistent-agent",
495+
});
496+
497+
expect(captureSpy).toHaveBeenCalledWith(
498+
"oss.runtime.copilot_request_created",
499+
expect.objectContaining({
500+
requestType: "connect",
501+
"cloud.api_key_provided": false,
502+
}),
503+
);
504+
} finally {
505+
captureSpy.mockRestore();
506+
}
507+
});
508+
509+
it("includes cloud.public_api_key when x-copilotcloud-public-api-key header is set", async () => {
510+
const { telemetry } = await import("../telemetry");
511+
const captureSpy = vi
512+
.spyOn(telemetry, "capture")
513+
.mockResolvedValue(undefined);
514+
515+
try {
516+
const runtime = createMockRuntime({});
517+
const request = new Request("https://example.com/agent/test/connect", {
518+
method: "POST",
519+
headers: {
520+
"x-copilotcloud-public-api-key": "ck_pub_connect_test",
521+
},
522+
});
523+
524+
await handleConnectAgent({
525+
runtime,
526+
request,
527+
agentId: "nonexistent-agent",
528+
});
529+
530+
expect(captureSpy).toHaveBeenCalledWith(
531+
"oss.runtime.copilot_request_created",
532+
expect.objectContaining({
533+
"cloud.api_key_provided": true,
534+
"cloud.public_api_key": "ck_pub_connect_test",
535+
}),
536+
);
537+
} finally {
538+
captureSpy.mockRestore();
539+
}
540+
});
541+
});
478542
});

packages/runtime/src/v2/runtime/__tests__/handle-run.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,4 +938,66 @@ describe("handleRunAgent", () => {
938938
}
939939
});
940940
});
941+
942+
describe("telemetry", () => {
943+
it("captures oss.runtime.copilot_request_created on every invocation", async () => {
944+
// Dynamic import so we spy on the module singleton the handler uses.
945+
const { telemetry } = await import("../telemetry");
946+
const captureSpy = vi
947+
.spyOn(telemetry, "capture")
948+
.mockResolvedValue(undefined);
949+
950+
try {
951+
const runtime = createMockRuntime({});
952+
await handleRunAgent({
953+
runtime,
954+
request: createMockRequest(),
955+
agentId: "nonexistent-agent",
956+
});
957+
958+
expect(captureSpy).toHaveBeenCalledWith(
959+
"oss.runtime.copilot_request_created",
960+
expect.objectContaining({
961+
requestType: "run",
962+
"cloud.api_key_provided": false,
963+
}),
964+
);
965+
} finally {
966+
captureSpy.mockRestore();
967+
}
968+
});
969+
970+
it("includes cloud.public_api_key when x-copilotcloud-public-api-key header is set", async () => {
971+
const { telemetry } = await import("../telemetry");
972+
const captureSpy = vi
973+
.spyOn(telemetry, "capture")
974+
.mockResolvedValue(undefined);
975+
976+
try {
977+
const runtime = createMockRuntime({});
978+
const request = new Request("https://example.com/agent/test/run", {
979+
method: "POST",
980+
headers: {
981+
"x-copilotcloud-public-api-key": "ck_pub_run_test",
982+
},
983+
});
984+
985+
await handleRunAgent({
986+
runtime,
987+
request,
988+
agentId: "nonexistent-agent",
989+
});
990+
991+
expect(captureSpy).toHaveBeenCalledWith(
992+
"oss.runtime.copilot_request_created",
993+
expect.objectContaining({
994+
"cloud.api_key_provided": true,
995+
"cloud.public_api_key": "ck_pub_run_test",
996+
}),
997+
);
998+
} finally {
999+
captureSpy.mockRestore();
1000+
}
1001+
});
1002+
});
9411003
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Integration test: Hono single-route adapter (deprecated path) + telemetry.
3+
*
4+
* `createCopilotEndpointSingleRoute` is the legacy direct single-route entry
5+
* point, superseded by `createCopilotHonoHandler({ mode: "single-route" })`
6+
* but still exported.
7+
*/
8+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9+
import type { AbstractAgent } from "@ag-ui/client";
10+
11+
import { telemetry } from "../telemetry";
12+
import { createCopilotEndpointSingleRoute } from "../endpoints/hono-single";
13+
import { CopilotRuntime } from "../core/runtime";
14+
15+
function makeAgent(): AbstractAgent {
16+
const a: unknown = { execute: async () => ({ events: [] }) };
17+
(a as { clone: () => unknown }).clone = () => makeAgent();
18+
return a as AbstractAgent;
19+
}
20+
21+
describe("Hono single-route adapter — telemetry firing (integration)", () => {
22+
let captureSpy: ReturnType<typeof vi.spyOn>;
23+
24+
beforeEach(() => {
25+
captureSpy = vi.spyOn(telemetry, "capture").mockResolvedValue(undefined);
26+
});
27+
28+
afterEach(() => {
29+
captureSpy.mockRestore();
30+
});
31+
32+
it("fires instance_created on handler creation", async () => {
33+
const runtime = new CopilotRuntime({ agents: { default: makeAgent() } });
34+
createCopilotEndpointSingleRoute({ runtime, basePath: "/api/copilotkit" });
35+
36+
await vi.waitFor(() => {
37+
expect(captureSpy).toHaveBeenCalledWith(
38+
"oss.runtime.instance_created",
39+
expect.objectContaining({
40+
agentsAmount: 1,
41+
"cloud.api_key_provided": false,
42+
}),
43+
);
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)