@copilotkit/web-inspector 1.64.2-canary.perfall1 → 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.
@@ -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({ banner_id: "ts-2025", cta_label: "Try threads" });
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();
@@ -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
- function isThreadsTelemetryEvent(event: TelemetryEvent): boolean {
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 threadsProperties = isThreadsTelemetryEvent(event)
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
- ...threadsProperties,
168
+ ...enrichedProperties,
160
169
  distinct_id: distinctId,
161
170
  },
162
171
  package: {
163
172
  name: PACKAGE_NAME,
164
- ...(isThreadsTelemetryEvent(event) ? { version: PACKAGE_VERSION } : {}),
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 = {