Skip to content

Commit ce35cba

Browse files
marthakellyclaude
authored andcommitted
feat(inspector/telemetry): propagate telemetryDisabled from runtime env var through inspector
- Add telemetryDisabled to RuntimeInfo from COPILOTKIT_TELEMETRY_DISABLED/DO_NOT_TRACK env vars - Mirror through AgentRegistry and expose via CopilotKitCore getter - Guard track calls, URL param appending, and console disclosure on core.telemetryDisabled - Move maybeShowDisclosure() to onRuntimeConnectionStatusChanged (fires after core attaches) - Update docs to replace localStorage toggle description with env var approach - Add telemetryDisabled test suite to get-runtime-info tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 78e8c50 commit ce35cba

7 files changed

Lines changed: 104 additions & 38 deletions

File tree

docs/snippets/shared/telemetry/anonymous.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ In CopilotRuntime, simply set `COPILOTKIT_TELEMETRY_DISABLED=true`. We also resp
1515

1616
Alternatively, you can directly set the `telemetryDisabled` flag to `true` when configuring your Copilot Runtime endpoint.
1717

18-
In the **Inspector** dev-console, open the **Privacy** tab and toggle off **Send anonymous usage data**. The setting is stored in your browser's localStorage and short-circuits all sends from the inspector.
18+
For the **Inspector** dev-console, set `COPILOTKIT_TELEMETRY_DISABLED=true` in your project's environment variables. The inspector reads this setting from the runtime at startup and suppresses all anonymous interaction events.
1919

2020
## How to adjust telemetry sample rate
2121

@@ -35,7 +35,7 @@ Every event also carries a `distinct_id` (a UUID v4 generated on first inspector
3535

3636
When the inspector renders a banner CTA link, it appends the same anonymous `distinct_id` to outbound URLs as a `posthog_distinct_id` query parameter. This lets the destination site (typically `copilotkit.ai`) bridge the visit back to the inspector session for funnel correlation, without storing any cookie or identifier from the host page. The URL parameter is suppressed when you opt out.
3737

38-
The inspector telemetry pipeline never sends message content, agent state, prompts, completions, banner markdown text, or any payload you inspect. The opt-out toggle covers all anonymous interaction telemetry, including any future signup attribution.
38+
The inspector telemetry pipeline never sends message content, agent state, prompts, completions, banner markdown text, or any payload you inspect. Setting `COPILOTKIT_TELEMETRY_DISABLED=true` suppresses all anonymous interaction telemetry from the inspector, including any future signup attribution.
3939

4040
## Get in touch
4141

packages/core/src/core/agent-registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class AgentRegistry {
6969
private _a2uiEnabled: boolean = false;
7070
private _openGenerativeUIEnabled: boolean = false;
7171
private _licenseStatus?: RuntimeLicenseStatus;
72+
private _telemetryDisabled: boolean = false;
7273

7374
constructor(private core: CopilotKitCore) {}
7475

@@ -119,6 +120,10 @@ export class AgentRegistry {
119120
return this._licenseStatus;
120121
}
121122

123+
get telemetryDisabled(): boolean {
124+
return this._telemetryDisabled;
125+
}
126+
122127
/**
123128
* Initialize agents from configuration
124129
*/
@@ -404,6 +409,7 @@ export class AgentRegistry {
404409
this._openGenerativeUIEnabled =
405410
runtimeInfoResponse.openGenerativeUIEnabled ?? false;
406411
this._licenseStatus = runtimeInfoResponse.licenseStatus;
412+
this._telemetryDisabled = runtimeInfoResponse.telemetryDisabled ?? false;
407413

408414
await this.notifyRuntimeStatusChanged(
409415
CopilotKitCoreRuntimeConnectionStatus.Connected,

packages/core/src/core/core.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,10 @@ export class CopilotKitCore {
628628
return this.agentRegistry.licenseStatus;
629629
}
630630

631+
get telemetryDisabled(): boolean {
632+
return this.agentRegistry.telemetryDisabled;
633+
}
634+
631635
/**
632636
* Configuration updates
633637
*/

packages/runtime/src/v2/runtime/__tests__/get-runtime-info.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
22
import { CopilotRuntime } from "../core/runtime";
33
import { TranscriptionService } from "../transcription-service/transcription-service";
4-
import { describe, it, expect, vi } from "vitest";
4+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
55
import type { AbstractAgent } from "@ag-ui/client";
66

77
// Mock transcription service
@@ -35,6 +35,7 @@ describe("handleGetRuntimeInfo", () => {
3535
mode: "sse",
3636
a2uiEnabled: false,
3737
openGenerativeUIEnabled: false,
38+
telemetryDisabled: false,
3839
});
3940
});
4041

@@ -60,6 +61,7 @@ describe("handleGetRuntimeInfo", () => {
6061
mode: "sse",
6162
a2uiEnabled: false,
6263
openGenerativeUIEnabled: false,
64+
telemetryDisabled: false,
6365
});
6466
});
6567

@@ -97,6 +99,7 @@ describe("handleGetRuntimeInfo", () => {
9799
mode: "sse",
98100
a2uiEnabled: false,
99101
openGenerativeUIEnabled: false,
102+
telemetryDisabled: false,
100103
});
101104
});
102105

@@ -250,6 +253,61 @@ describe("handleGetRuntimeInfo", () => {
250253
warnSpy.mockRestore();
251254
});
252255

256+
describe("telemetryDisabled", () => {
257+
beforeEach(() => {
258+
delete process.env.COPILOTKIT_TELEMETRY_DISABLED;
259+
delete process.env.DO_NOT_TRACK;
260+
});
261+
262+
afterEach(() => {
263+
delete process.env.COPILOTKIT_TELEMETRY_DISABLED;
264+
delete process.env.DO_NOT_TRACK;
265+
});
266+
267+
it("returns telemetryDisabled: false when env var is not set", async () => {
268+
const runtime = new CopilotRuntime({ agents: {} });
269+
const response = await handleGetRuntimeInfo({
270+
runtime,
271+
request: mockRequest,
272+
});
273+
const data = await response.json();
274+
expect(data.telemetryDisabled).toBe(false);
275+
});
276+
277+
it("returns telemetryDisabled: true when COPILOTKIT_TELEMETRY_DISABLED=true", async () => {
278+
process.env.COPILOTKIT_TELEMETRY_DISABLED = "true";
279+
const runtime = new CopilotRuntime({ agents: {} });
280+
const response = await handleGetRuntimeInfo({
281+
runtime,
282+
request: mockRequest,
283+
});
284+
const data = await response.json();
285+
expect(data.telemetryDisabled).toBe(true);
286+
});
287+
288+
it("returns telemetryDisabled: true when COPILOTKIT_TELEMETRY_DISABLED=1", async () => {
289+
process.env.COPILOTKIT_TELEMETRY_DISABLED = "1";
290+
const runtime = new CopilotRuntime({ agents: {} });
291+
const response = await handleGetRuntimeInfo({
292+
runtime,
293+
request: mockRequest,
294+
});
295+
const data = await response.json();
296+
expect(data.telemetryDisabled).toBe(true);
297+
});
298+
299+
it("returns telemetryDisabled: true when DO_NOT_TRACK=1", async () => {
300+
process.env.DO_NOT_TRACK = "1";
301+
const runtime = new CopilotRuntime({ agents: {} });
302+
const response = await handleGetRuntimeInfo({
303+
runtime,
304+
request: mockRequest,
305+
});
306+
const data = await response.json();
307+
expect(data.telemetryDisabled).toBe(true);
308+
});
309+
});
310+
253311
it("should return 500 error when runtime.agents throws an error", async () => {
254312
const runtime = {
255313
get agents(): Record<string, AbstractAgent> {

packages/runtime/src/v2/runtime/handlers/get-runtime-info.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import type { AgentCapabilities } from "@ag-ui/core";
2-
import {
3-
CopilotRuntimeLike,
4-
isIntelligenceRuntime,
5-
resolveAgents,
6-
} from "../core/runtime";
7-
import {
8-
AgentDescription,
9-
RuntimeInfo,
10-
type RuntimeLicenseStatus,
11-
} from "@copilotkit/shared";
2+
import type { CopilotRuntimeLike } from "../core/runtime";
3+
import { isIntelligenceRuntime, resolveAgents } from "../core/runtime";
4+
import type { AgentDescription, RuntimeInfo } from "@copilotkit/shared";
5+
import { type RuntimeLicenseStatus } from "@copilotkit/shared";
126
import { VERSION } from "../core/runtime";
7+
import { isTelemetryDisabled } from "../telemetry/telemetry-client";
138

149
function resolveLicenseStatus(
1510
runtime: CopilotRuntimeLike,
@@ -84,6 +79,7 @@ export async function handleGetRuntimeInfo({
8479
...(isIntelligenceRuntime(runtime)
8580
? { licenseStatus: resolveLicenseStatus(runtime) }
8681
: {}),
82+
telemetryDisabled: isTelemetryDisabled(),
8783
};
8884

8985
return new Response(JSON.stringify(runtimeInfo), {

packages/shared/src/utils/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ export interface RuntimeInfo {
4848
a2uiEnabled?: boolean;
4949
openGenerativeUIEnabled?: boolean;
5050
licenseStatus?: RuntimeLicenseStatus;
51+
telemetryDisabled?: boolean;
5152
}

packages/web-inspector/src/index.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,6 +2630,9 @@ export class WebInspectorElement extends LitElement {
26302630
onRuntimeConnectionStatusChanged: ({ status }) => {
26312631
this.runtimeStatus = status;
26322632
if (status === "connected") {
2633+
if (!core.telemetryDisabled) {
2634+
maybeShowDisclosure();
2635+
}
26332636
for (const agentId of this._ownedThreadStores.keys()) {
26342637
this.refreshOwnedThreadStore(agentId);
26352638
}
@@ -4188,10 +4191,6 @@ ${argsString}</pre
41884191
return;
41894192
}
41904193

4191-
// First-run console disclosure for anonymous interaction telemetry.
4192-
// No-ops when the user has opted out or has already seen the message.
4193-
maybeShowDisclosure();
4194-
41954194
// Ensure the anonymous distinct-ID is set on inspector load (per
41964195
// OSS-96 acceptance criteria), so it's available for cross-domain
41974196
// propagation onto banner-CTA links even before the first event
@@ -5669,31 +5668,30 @@ ${argsString}</pre
56695668
}
56705669

56715670
private renderSettingsPanel() {
5671+
const optedOut = this.core?.telemetryDisabled ?? false;
56725672
return html`
56735673
<div class="flex h-full flex-col overflow-hidden">
56745674
<div class="overflow-auto p-4">
5675-
<div class="mx-auto max-w-2xl space-y-6">
5676-
<div>
5677-
<h2 class="text-base font-semibold text-slate-900">Settings</h2>
5678-
</div>
5675+
<div class="space-y-3">
5676+
<h2 class="text-sm font-semibold text-slate-900">Settings</h2>
56795677
5680-
<div class="space-y-3">
5681-
<h3 class="text-sm font-medium text-slate-700">Privacy</h3>
5678+
<div class="space-y-2">
5679+
<h3 class="text-sm text-slate-500">Privacy</h3>
56825680
<div class="rounded-lg border border-slate-200 bg-white p-4 space-y-3">
5683-
<p class="text-sm text-gray-600">
5684-
CopilotKit collects anonymous interaction events from the
5685-
inspector (banner views, clicks, tab navigation) so we know
5686-
which features people use. We never collect message content,
5687-
agent state, prompts, completions, or any payload you inspect.
5681+
<p class="text-sm text-gray-600 flex items-start gap-2">
5682+
<span>${optedOut ? "❌" : "✅"}</span>
5683+
<span>
5684+
${optedOut
5685+
? "You have disabled anonymous interaction data collection."
5686+
: "CopilotKit is currently collecting anonymous interaction data from the inspector so we know which features people use. We never collect message content, agent state, prompts, or completions."}
5687+
</span>
56885688
</p>
56895689
<a
56905690
class="inline-flex items-center gap-1 text-sm text-slate-700 underline hover:text-slate-900"
56915691
href=${TELEMETRY_DOCS_URL}
56925692
target="_blank"
56935693
rel="noopener"
5694-
>
5695-
I want to opt out — show me how →
5696-
</a>
5694+
>Learn more →</a>
56975695
</div>
56985696
</div>
56995697
</div>
@@ -5705,6 +5703,7 @@ ${argsString}</pre
57055703
// Fires `banner_clicked` at most once per `${bannerId}:${cta}` per mount so
57065704
// copy-button retries and accidental multi-clicks don't inflate funnel counts.
57075705
private trackBannerClickedOnce(opts: { cta: "body" | "dismiss" }): void {
5706+
if (this.core?.telemetryDisabled) return;
57085707
const id = this.announcementTimestamp;
57095708
if (!id) return;
57105709
const key = `${id}:${opts.cta}`;
@@ -6484,7 +6483,7 @@ ${prettyEvent}</pre
64846483
}
64856484

64866485
private handleMenuSelect(key: MenuKey): void {
6487-
if (!this.menuItems.some((item) => item.key === key)) {
6486+
if (key !== "settings" && !this.menuItems.some((item) => item.key === key)) {
64886487
return;
64896488
}
64906489

@@ -6523,7 +6522,7 @@ ${prettyEvent}</pre
65236522
}
65246523

65256524
if (key === "threads") {
6526-
if (this.selectedMenu !== "threads") {
6525+
if (this.selectedMenu !== "threads" && !this.core?.telemetryDisabled) {
65276526
trackThreadsTabClicked();
65286527
}
65296528
this.autoSelectLatestThread();
@@ -7499,10 +7498,12 @@ ${prettyEvent}</pre
74997498
!this.viewedBannerTimestamps.has(timestamp)
75007499
) {
75017500
this.viewedBannerTimestamps.add(timestamp);
7502-
trackBannerViewed({
7503-
banner_id: timestamp,
7504-
cta_label: ctaLabel ?? undefined,
7505-
});
7501+
if (!this.core?.telemetryDisabled) {
7502+
trackBannerViewed({
7503+
banner_id: timestamp,
7504+
cta_label: ctaLabel ?? undefined,
7505+
});
7506+
}
75067507
}
75077508

75087509
this.requestUpdate();
@@ -7617,7 +7618,7 @@ ${prettyEvent}</pre
76177618
// close the banner_viewed → banner_clicked → signup_attributed
76187619
// funnel. Returns null when the user has opted out, so opt-out
76197620
// suppresses cross-domain ID leaks too.
7620-
if (!url.searchParams.has("posthog_distinct_id")) {
7621+
if (!url.searchParams.has("posthog_distinct_id") && !this.core?.telemetryDisabled) {
76217622
const distinctId = getTelemetryDistinctIdForUrl();
76227623
if (distinctId) {
76237624
url.searchParams.append("posthog_distinct_id", distinctId);

0 commit comments

Comments
 (0)