@copilotkit/web-inspector 1.64.2-canary.rc-1 → 1.64.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +81 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +82 -25
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +137 -56
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/telemetry.cjs +28 -5
- package/dist/lib/telemetry.cjs.map +1 -1
- package/dist/lib/telemetry.mjs +27 -6
- package/dist/lib/telemetry.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/telemetry-egress-guard.spec.ts +41 -0
- package/src/__tests__/web-inspector.spec.ts +343 -3
- package/src/index.ts +137 -35
- package/src/lib/__tests__/telemetry.test.ts +97 -3
- package/src/lib/telemetry.ts +77 -4
- package/src/lib/testing/telemetry-egress-guard.ts +33 -0
- package/vitest.setup.ts +8 -0
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
maybeShowDisclosure,
|
|
14
14
|
track,
|
|
15
15
|
trackBannerClicked,
|
|
16
|
+
trackBannerDismissed,
|
|
16
17
|
trackBannerViewed,
|
|
18
|
+
trackInspectorOpened,
|
|
17
19
|
trackTalkToEngineerClicked,
|
|
18
20
|
trackThreadsEmptyEnabledViewed,
|
|
19
21
|
trackThreadsEnabledViewed,
|
|
@@ -162,8 +164,12 @@ describe("track()", () => {
|
|
|
162
164
|
// ─── Typed per-event helpers ─────────────────────────────────────────────────
|
|
163
165
|
|
|
164
166
|
describe("typed helpers", () => {
|
|
165
|
-
it("trackBannerViewed sends banner_id and optional cta_label", async () => {
|
|
166
|
-
trackBannerViewed({
|
|
167
|
+
it("trackBannerViewed sends banner_id, surface, and optional cta_label", async () => {
|
|
168
|
+
trackBannerViewed({
|
|
169
|
+
banner_id: "ts-2025",
|
|
170
|
+
surface: "collapsed_preview",
|
|
171
|
+
cta_label: "Try threads",
|
|
172
|
+
});
|
|
167
173
|
await Promise.resolve();
|
|
168
174
|
const [, init] = fetchMock.mock.calls[0]!;
|
|
169
175
|
const body = JSON.parse((init?.body as string) ?? "{}") as {
|
|
@@ -172,17 +178,105 @@ describe("typed helpers", () => {
|
|
|
172
178
|
};
|
|
173
179
|
expect(body.event).toBe(TELEMETRY_EVENTS.bannerViewed);
|
|
174
180
|
expect(body.properties.banner_id).toBe("ts-2025");
|
|
181
|
+
expect(body.properties.surface).toBe("collapsed_preview");
|
|
175
182
|
expect(body.properties.cta_label).toBe("Try threads");
|
|
176
183
|
});
|
|
177
184
|
|
|
178
185
|
it("trackBannerViewed omits cta_label when undefined (JSON.stringify drops it)", async () => {
|
|
179
|
-
trackBannerViewed({ banner_id: "ts-2025" });
|
|
186
|
+
trackBannerViewed({ banner_id: "ts-2025", surface: "expanded_card" });
|
|
180
187
|
await Promise.resolve();
|
|
181
188
|
const [, init] = fetchMock.mock.calls[0]!;
|
|
182
189
|
const raw = (init?.body as string) ?? "{}";
|
|
183
190
|
expect(raw).not.toContain("cta_label");
|
|
184
191
|
});
|
|
185
192
|
|
|
193
|
+
it("trackBannerDismissed sends a first-class dismissed event with its surface", async () => {
|
|
194
|
+
trackBannerDismissed({
|
|
195
|
+
banner_id: "ts-2025",
|
|
196
|
+
surface: "expanded_card",
|
|
197
|
+
cta_label: "Try threads",
|
|
198
|
+
});
|
|
199
|
+
await Promise.resolve();
|
|
200
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
201
|
+
const body = JSON.parse((init?.body as string) ?? "{}") as {
|
|
202
|
+
event: string;
|
|
203
|
+
properties: Record<string, unknown>;
|
|
204
|
+
package: { name: string; version?: string };
|
|
205
|
+
};
|
|
206
|
+
expect(body.event).toBe("oss.inspector.banner_dismissed");
|
|
207
|
+
expect(body.properties.banner_id).toBe("ts-2025");
|
|
208
|
+
expect(body.properties.surface).toBe("expanded_card");
|
|
209
|
+
expect(body.properties.cta_label).toBe("Try threads");
|
|
210
|
+
// Banner events keep the flat shape their existing dashboards read.
|
|
211
|
+
expect(body.properties).not.toHaveProperty("package_version");
|
|
212
|
+
expect(body.package).toEqual({ name: "@copilotkit/web-inspector" });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("trackInspectorOpened sends open_source and is enriched with package identity", async () => {
|
|
216
|
+
trackInspectorOpened({
|
|
217
|
+
open_source: "floating_button",
|
|
218
|
+
license_status: "none",
|
|
219
|
+
runtime_mode: "sse",
|
|
220
|
+
runtime_url_type: "localhost",
|
|
221
|
+
has_unseen_announcement: true,
|
|
222
|
+
});
|
|
223
|
+
await Promise.resolve();
|
|
224
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
225
|
+
const body = JSON.parse((init?.body as string) ?? "{}") as {
|
|
226
|
+
event: string;
|
|
227
|
+
properties: Record<string, unknown>;
|
|
228
|
+
package: { name: string; version?: string };
|
|
229
|
+
};
|
|
230
|
+
expect(body.event).toBe("oss.inspector.opened");
|
|
231
|
+
expect(body.properties).toMatchObject({
|
|
232
|
+
open_source: "floating_button",
|
|
233
|
+
license_status: "none",
|
|
234
|
+
runtime_mode: "sse",
|
|
235
|
+
runtime_url_type: "localhost",
|
|
236
|
+
has_unseen_announcement: true,
|
|
237
|
+
package_name: "@copilotkit/web-inspector",
|
|
238
|
+
package_version: webInspectorPackage.version,
|
|
239
|
+
});
|
|
240
|
+
expect(body.properties.inspector_distinct_id).toBe(
|
|
241
|
+
body.properties.distinct_id,
|
|
242
|
+
);
|
|
243
|
+
expect(body.package).toEqual({
|
|
244
|
+
name: "@copilotkit/web-inspector",
|
|
245
|
+
version: webInspectorPackage.version,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("opened and banner events carry no message, state, or announcement content", async () => {
|
|
250
|
+
trackInspectorOpened({ open_source: "announcement_preview" });
|
|
251
|
+
trackBannerDismissed({
|
|
252
|
+
banner_id: "ts-2025",
|
|
253
|
+
surface: "collapsed_preview",
|
|
254
|
+
});
|
|
255
|
+
await Promise.resolve();
|
|
256
|
+
for (const [, init] of fetchMock.mock.calls) {
|
|
257
|
+
const properties = (
|
|
258
|
+
JSON.parse((init?.body as string) ?? "{}") as {
|
|
259
|
+
properties: Record<string, unknown>;
|
|
260
|
+
}
|
|
261
|
+
).properties;
|
|
262
|
+
// Allow-list assertion: any new key has to be added here deliberately,
|
|
263
|
+
// so an accidental content/PII payload fails the test instead of
|
|
264
|
+
// shipping.
|
|
265
|
+
const allowed = new Set([
|
|
266
|
+
"open_source",
|
|
267
|
+
"banner_id",
|
|
268
|
+
"surface",
|
|
269
|
+
"distinct_id",
|
|
270
|
+
"inspector_distinct_id",
|
|
271
|
+
"package_name",
|
|
272
|
+
"package_version",
|
|
273
|
+
]);
|
|
274
|
+
expect(Object.keys(properties).filter((k) => !allowed.has(k))).toEqual(
|
|
275
|
+
[],
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
186
280
|
it("trackBannerClicked sends banner_id, cta, and optional cta_label", async () => {
|
|
187
281
|
trackBannerClicked({ banner_id: "ts-2025", cta: "body" });
|
|
188
282
|
await Promise.resolve();
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -27,8 +27,10 @@ import packageJson from "../../package.json" with { type: "json" };
|
|
|
27
27
|
// owned-prefix gate (oss-path-to-production) can accept them server-side
|
|
28
28
|
// without a per-event sink deploy.
|
|
29
29
|
export const TELEMETRY_EVENTS = {
|
|
30
|
+
opened: "oss.inspector.opened",
|
|
30
31
|
bannerViewed: "oss.inspector.banner_viewed",
|
|
31
32
|
bannerClicked: "oss.inspector.banner_clicked",
|
|
33
|
+
bannerDismissed: "oss.inspector.banner_dismissed",
|
|
32
34
|
threadsTabClicked: "oss.inspector.threads_tab_clicked",
|
|
33
35
|
threadsLockedViewed: "oss.inspector.threads_locked_viewed",
|
|
34
36
|
threadsIntelligenceSignupClicked:
|
|
@@ -69,8 +71,15 @@ const PACKAGE_VERSION = packageJson.version;
|
|
|
69
71
|
// runtime's existing scarf-client convention.
|
|
70
72
|
const FETCH_TIMEOUT_MS = 3000;
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
// Events that carry package identity (`package_name` / `package_version`) and
|
|
75
|
+
// the inspector's anonymous distinct-ID alongside their own properties.
|
|
76
|
+
// Threads / memories events have always been enriched this way; `opened`
|
|
77
|
+
// joins them so panel-open volume can be segmented by inspector version.
|
|
78
|
+
// Banner events deliberately keep the flat shape their existing dashboards
|
|
79
|
+
// read.
|
|
80
|
+
function isEnrichedTelemetryEvent(event: TelemetryEvent): boolean {
|
|
73
81
|
return (
|
|
82
|
+
event === TELEMETRY_EVENTS.opened ||
|
|
74
83
|
event === TELEMETRY_EVENTS.threadsTabClicked ||
|
|
75
84
|
event === TELEMETRY_EVENTS.threadsLockedViewed ||
|
|
76
85
|
event === TELEMETRY_EVENTS.threadsIntelligenceSignupClicked ||
|
|
@@ -143,7 +152,7 @@ export function track(
|
|
|
143
152
|
if (isTelemetryOptedOut()) return;
|
|
144
153
|
|
|
145
154
|
const distinctId = getOrCreateTelemetryDistinctId();
|
|
146
|
-
const
|
|
155
|
+
const enrichedProperties = isEnrichedTelemetryEvent(event)
|
|
147
156
|
? {
|
|
148
157
|
package_name: PACKAGE_NAME,
|
|
149
158
|
package_version: PACKAGE_VERSION,
|
|
@@ -156,12 +165,14 @@ export function track(
|
|
|
156
165
|
event,
|
|
157
166
|
properties: {
|
|
158
167
|
...properties,
|
|
159
|
-
...
|
|
168
|
+
...enrichedProperties,
|
|
160
169
|
distinct_id: distinctId,
|
|
161
170
|
},
|
|
162
171
|
package: {
|
|
163
172
|
name: PACKAGE_NAME,
|
|
164
|
-
...(
|
|
173
|
+
...(isEnrichedTelemetryEvent(event)
|
|
174
|
+
? { version: PACKAGE_VERSION }
|
|
175
|
+
: {}),
|
|
165
176
|
},
|
|
166
177
|
ts: Math.floor(Date.now() / 1000),
|
|
167
178
|
});
|
|
@@ -176,8 +187,18 @@ export function track(
|
|
|
176
187
|
// These enforce the known property shape for each V1 event at the call
|
|
177
188
|
// site, so callers can't accidentally include PII under a wrong key.
|
|
178
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Where an announcement was rendered when the event fired. The announcement
|
|
192
|
+
* has two surfaces — the preview bubble beside the *collapsed* floating
|
|
193
|
+
* button, and the card *inside* the opened panel — and reach on one says
|
|
194
|
+
* nothing about attention on the other. Always stamped at fire time, never
|
|
195
|
+
* inferred at fetch time.
|
|
196
|
+
*/
|
|
197
|
+
export type BannerSurface = "collapsed_preview" | "expanded_card";
|
|
198
|
+
|
|
179
199
|
export function trackBannerViewed(props: {
|
|
180
200
|
banner_id: string;
|
|
201
|
+
surface: BannerSurface;
|
|
181
202
|
cta_label?: string;
|
|
182
203
|
}): void {
|
|
183
204
|
track(TELEMETRY_EVENTS.bannerViewed, props);
|
|
@@ -191,6 +212,58 @@ export function trackBannerClicked(props: {
|
|
|
191
212
|
track(TELEMETRY_EVENTS.bannerClicked, props);
|
|
192
213
|
}
|
|
193
214
|
|
|
215
|
+
/**
|
|
216
|
+
* First-class dismissal signal. Emitted *in addition to*
|
|
217
|
+
* `banner_clicked { cta: "dismiss" }` rather than replacing it — the
|
|
218
|
+
* existing dashboards read the `cta` value, so removing it would silently
|
|
219
|
+
* zero them out. New reporting should prefer this event, whose `surface`
|
|
220
|
+
* says whether the user swatted the bubble away or dismissed the card
|
|
221
|
+
* after opening the panel.
|
|
222
|
+
*/
|
|
223
|
+
export function trackBannerDismissed(props: {
|
|
224
|
+
banner_id: string;
|
|
225
|
+
surface: BannerSurface;
|
|
226
|
+
cta_label?: string;
|
|
227
|
+
}): void {
|
|
228
|
+
track(TELEMETRY_EVENTS.bannerDismissed, props);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* How the panel was opened. Restoring a persisted-open panel on mount is
|
|
233
|
+
* deliberately NOT a source: it is not an open *intent*, and counting it
|
|
234
|
+
* would turn every dev-server hot reload into an "open".
|
|
235
|
+
*/
|
|
236
|
+
export type InspectorOpenSource = "floating_button" | "announcement_preview";
|
|
237
|
+
|
|
238
|
+
export type InspectorOpenedTelemetryProps = {
|
|
239
|
+
open_source: InspectorOpenSource;
|
|
240
|
+
package_name?: typeof PACKAGE_NAME;
|
|
241
|
+
package_version?: string;
|
|
242
|
+
inspector_distinct_id?: string;
|
|
243
|
+
license_status?:
|
|
244
|
+
| "valid"
|
|
245
|
+
| "none"
|
|
246
|
+
| "expired"
|
|
247
|
+
| "expiring"
|
|
248
|
+
| "invalid"
|
|
249
|
+
| "unknown";
|
|
250
|
+
runtime_mode?: "sse" | "intelligence";
|
|
251
|
+
runtime_url_type?: RuntimeUrlType;
|
|
252
|
+
/** True when an unseen announcement was on screen at open time. */
|
|
253
|
+
has_unseen_announcement?: boolean;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Panel-open signal (OSS-566). Before this event, opens could only be
|
|
258
|
+
* inferred from in-panel activity (a floor) or from `banner_clicked`
|
|
259
|
+
* cta=`body` (which misses the floating-button path entirely).
|
|
260
|
+
*/
|
|
261
|
+
export function trackInspectorOpened(
|
|
262
|
+
props: InspectorOpenedTelemetryProps,
|
|
263
|
+
): void {
|
|
264
|
+
track(TELEMETRY_EVENTS.opened, props);
|
|
265
|
+
}
|
|
266
|
+
|
|
194
267
|
export type InspectorThreadTelemetryProps = {
|
|
195
268
|
package_name?: typeof PACKAGE_NAME;
|
|
196
269
|
package_version?: string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Test-only helper (imported by vitest.setup.ts — never by the bundle entry).
|
|
2
|
+
//
|
|
3
|
+
// This suite runs in jsdom, where a real `fetch` exists. Inspector telemetry is
|
|
4
|
+
// browser-side and fire-and-forget, so any test that drives a banner / threads /
|
|
5
|
+
// open code path without stubbing fetch POSTs a real `oss.inspector.*` event to
|
|
6
|
+
// the live sink — from a developer's machine and from every CI run — polluting
|
|
7
|
+
// the dataset with traffic indistinguishable from user activity. The
|
|
8
|
+
// announcement-dismissal tests did exactly this before this guard existed.
|
|
9
|
+
//
|
|
10
|
+
// No environment variable can prevent it: the inspector's opt-out is the
|
|
11
|
+
// runtime's `/info` response, and these tests never boot a runtime.
|
|
12
|
+
//
|
|
13
|
+
// The guard swallows requests to the sink and delegates everything else, so
|
|
14
|
+
// tests that stub or spy on fetch themselves are unaffected.
|
|
15
|
+
|
|
16
|
+
export const TELEMETRY_SINK_ORIGIN = "https://telemetry.copilotkit.ai";
|
|
17
|
+
|
|
18
|
+
export function createTelemetryEgressGuard(
|
|
19
|
+
realFetch: typeof fetch,
|
|
20
|
+
): typeof fetch {
|
|
21
|
+
return (input, init) => {
|
|
22
|
+
const url =
|
|
23
|
+
typeof input === "string"
|
|
24
|
+
? input
|
|
25
|
+
: input instanceof URL
|
|
26
|
+
? input.href
|
|
27
|
+
: input.url;
|
|
28
|
+
if (url?.startsWith(TELEMETRY_SINK_ORIGIN)) {
|
|
29
|
+
return Promise.resolve(new Response(null, { status: 204 }));
|
|
30
|
+
}
|
|
31
|
+
return realFetch(input, init);
|
|
32
|
+
};
|
|
33
|
+
}
|
package/vitest.setup.ts
CHANGED
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
import { beforeEach } from "vitest";
|
|
20
20
|
|
|
21
|
+
import { createTelemetryEgressGuard } from "./src/lib/testing/telemetry-egress-guard.js";
|
|
22
|
+
|
|
23
|
+
// No test may POST a real `oss.inspector.*` event to the live telemetry sink —
|
|
24
|
+
// see the helper for why no environment variable can cover this.
|
|
25
|
+
if (typeof globalThis.fetch === "function") {
|
|
26
|
+
globalThis.fetch = createTelemetryEgressGuard(globalThis.fetch);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
function createStorageShim(): Storage {
|
|
22
30
|
const store = new Map<string, string>();
|
|
23
31
|
const shim = {
|