@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
|
@@ -236,6 +236,26 @@ function getContextInternals(inspector: WebInspectorElement) {
|
|
|
236
236
|
return inspector as unknown as InspectorContextInternals;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
type TelemetryPost = { event: string; properties: Record<string, unknown> };
|
|
240
|
+
|
|
241
|
+
/** Decode the `oss.inspector.*` payloads a stubbed fetch received. */
|
|
242
|
+
function telemetryPostsFrom(fetchMock: {
|
|
243
|
+
mock: { calls: unknown[][] };
|
|
244
|
+
}): TelemetryPost[] {
|
|
245
|
+
return fetchMock.mock.calls
|
|
246
|
+
.filter(
|
|
247
|
+
(call) =>
|
|
248
|
+
String(call[0]) === "https://telemetry.copilotkit.ai/ingest" &&
|
|
249
|
+
(call[1] as RequestInit | undefined)?.method === "POST",
|
|
250
|
+
)
|
|
251
|
+
.map(
|
|
252
|
+
(call) =>
|
|
253
|
+
JSON.parse(
|
|
254
|
+
((call[1] as RequestInit | undefined)?.body as string) ?? "{}",
|
|
255
|
+
) as TelemetryPost,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
239
259
|
// --- Tests ---
|
|
240
260
|
|
|
241
261
|
describe("WebInspectorElement", () => {
|
|
@@ -1161,6 +1181,14 @@ describe("CpkThreadInspector provider contract", () => {
|
|
|
1161
1181
|
vi.stubGlobal("fetch", fetchMock);
|
|
1162
1182
|
const { el, internals } = createThreadInspector();
|
|
1163
1183
|
|
|
1184
|
+
// Count only thread-inspection requests. This stub replaces the global
|
|
1185
|
+
// fetch, so a fire-and-forget telemetry POST from an earlier case can land
|
|
1186
|
+
// here too; the assertion is about refetches, not total network calls.
|
|
1187
|
+
const threadFetches = () =>
|
|
1188
|
+
fetchMock.mock.calls.filter((call) =>
|
|
1189
|
+
String(call[0]).startsWith("http://runtime"),
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1164
1192
|
internals.runtimeUrl = "http://runtime";
|
|
1165
1193
|
internals.threadInspectionAvailable = true;
|
|
1166
1194
|
internals.headers = { Authorization: "Bearer first" };
|
|
@@ -1168,7 +1196,7 @@ describe("CpkThreadInspector provider contract", () => {
|
|
|
1168
1196
|
await flushProviderWork(el);
|
|
1169
1197
|
|
|
1170
1198
|
await vi.waitFor(() => {
|
|
1171
|
-
expect(
|
|
1199
|
+
expect(threadFetches()).toHaveLength(1);
|
|
1172
1200
|
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
1173
1201
|
auth: "Bearer first",
|
|
1174
1202
|
});
|
|
@@ -1178,12 +1206,12 @@ describe("CpkThreadInspector provider contract", () => {
|
|
|
1178
1206
|
await flushProviderWork(el);
|
|
1179
1207
|
|
|
1180
1208
|
await vi.waitFor(() => {
|
|
1181
|
-
expect(
|
|
1209
|
+
expect(threadFetches()).toHaveLength(2);
|
|
1182
1210
|
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
1183
1211
|
auth: "Bearer second",
|
|
1184
1212
|
});
|
|
1185
1213
|
});
|
|
1186
|
-
expect(headersOf(
|
|
1214
|
+
expect(headersOf(threadFetches().at(-1)!)).toMatchObject({
|
|
1187
1215
|
Authorization: "Bearer second",
|
|
1188
1216
|
});
|
|
1189
1217
|
});
|
|
@@ -1473,6 +1501,36 @@ describe("WebInspectorElement announcement preview dismissal", () => {
|
|
|
1473
1501
|
expect(a.isOpen).toBe(false);
|
|
1474
1502
|
});
|
|
1475
1503
|
|
|
1504
|
+
it("emits banner_dismissed with the bubble surface alongside banner_clicked", async () => {
|
|
1505
|
+
const fetchMock = vi
|
|
1506
|
+
.spyOn(globalThis, "fetch")
|
|
1507
|
+
.mockResolvedValue(new Response(null, { status: 204 }));
|
|
1508
|
+
const timestamp = "2026-06-11T13:00:00.000Z";
|
|
1509
|
+
const { inspector } = await mountWithUnseenAnnouncement(timestamp);
|
|
1510
|
+
|
|
1511
|
+
inspector.shadowRoot
|
|
1512
|
+
?.querySelector<HTMLElement>(".announcement-preview__dismiss")
|
|
1513
|
+
?.click();
|
|
1514
|
+
await inspector.updateComplete;
|
|
1515
|
+
|
|
1516
|
+
const posts = telemetryPostsFrom(fetchMock);
|
|
1517
|
+
const dismissed = posts.find(
|
|
1518
|
+
(post) => post.event === "oss.inspector.banner_dismissed",
|
|
1519
|
+
);
|
|
1520
|
+
expect(dismissed?.properties).toMatchObject({
|
|
1521
|
+
banner_id: timestamp,
|
|
1522
|
+
surface: "collapsed_preview",
|
|
1523
|
+
});
|
|
1524
|
+
// The legacy signal keeps flowing so existing dashboards don't zero out.
|
|
1525
|
+
const clicked = posts.find(
|
|
1526
|
+
(post) => post.event === "oss.inspector.banner_clicked",
|
|
1527
|
+
);
|
|
1528
|
+
expect(clicked?.properties).toMatchObject({
|
|
1529
|
+
banner_id: timestamp,
|
|
1530
|
+
cta: "dismiss",
|
|
1531
|
+
});
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1476
1534
|
it("clicking the popout body opens the inspector without persisting", async () => {
|
|
1477
1535
|
const { inspector, a } = await mountWithUnseenAnnouncement(
|
|
1478
1536
|
"2026-06-11T13:00:00.000Z",
|
|
@@ -1491,6 +1549,258 @@ describe("WebInspectorElement announcement preview dismissal", () => {
|
|
|
1491
1549
|
});
|
|
1492
1550
|
});
|
|
1493
1551
|
|
|
1552
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1553
|
+
// Panel-open + banner-surface telemetry (OSS-566 / OSS-568)
|
|
1554
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1555
|
+
//
|
|
1556
|
+
// `oss.inspector.opened` exists because opens were previously only inferable
|
|
1557
|
+
// from in-panel activity (a floor) or from `banner_clicked` cta=body (which
|
|
1558
|
+
// misses the floating-button path). `banner_viewed` carries a `surface`
|
|
1559
|
+
// because the bubble on the collapsed widget and the card inside the opened
|
|
1560
|
+
// panel are separate impressions — reach on one says nothing about the other.
|
|
1561
|
+
|
|
1562
|
+
const ANNOUNCEMENT_URL = "https://cdn.copilotkit.ai/announcements.json";
|
|
1563
|
+
|
|
1564
|
+
type OpenTelemetryInternals = {
|
|
1565
|
+
isOpen: boolean;
|
|
1566
|
+
announcementTimestamp: string | null;
|
|
1567
|
+
fetchAnnouncement: () => Promise<void>;
|
|
1568
|
+
openInspector: (source: "floating_button" | "announcement_preview") => void;
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
describe("WebInspectorElement open + banner surface telemetry", () => {
|
|
1572
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
1573
|
+
const timestamp = "2026-07-01T09:00:00.000Z";
|
|
1574
|
+
|
|
1575
|
+
beforeEach(() => {
|
|
1576
|
+
document.body.innerHTML = "";
|
|
1577
|
+
fetchMock = vi.fn((input: unknown) => {
|
|
1578
|
+
if (String(input) === ANNOUNCEMENT_URL) {
|
|
1579
|
+
return Promise.resolve(
|
|
1580
|
+
new Response(
|
|
1581
|
+
JSON.stringify({
|
|
1582
|
+
timestamp,
|
|
1583
|
+
previewText: "Channels are here",
|
|
1584
|
+
announcement: "Channels are here — [read more](https://x.test)",
|
|
1585
|
+
}),
|
|
1586
|
+
{ status: 200 },
|
|
1587
|
+
),
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return Promise.resolve(new Response(null, { status: 204 }));
|
|
1591
|
+
});
|
|
1592
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
afterEach(() => {
|
|
1596
|
+
vi.unstubAllGlobals();
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
/** Mount an inspector attached to a connected core with telemetry on. */
|
|
1600
|
+
function mount(telemetryDisabled = false, connected = true) {
|
|
1601
|
+
const { agent } = createMockAgent("alpha");
|
|
1602
|
+
const harness = createHeaderMockCore(
|
|
1603
|
+
{ alpha: agent },
|
|
1604
|
+
{},
|
|
1605
|
+
{},
|
|
1606
|
+
telemetryDisabled,
|
|
1607
|
+
);
|
|
1608
|
+
if (!connected) {
|
|
1609
|
+
harness.core.runtimeConnectionStatus =
|
|
1610
|
+
CopilotKitCoreRuntimeConnectionStatus.Disconnected;
|
|
1611
|
+
}
|
|
1612
|
+
const inspector = new WebInspectorElement();
|
|
1613
|
+
document.body.appendChild(inspector);
|
|
1614
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1615
|
+
return {
|
|
1616
|
+
inspector,
|
|
1617
|
+
harness,
|
|
1618
|
+
internals: inspector as unknown as OpenTelemetryInternals,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const posts = () => telemetryPostsFrom(fetchMock);
|
|
1623
|
+
const eventsNamed = (name: string) =>
|
|
1624
|
+
posts().filter((post) => post.event === name);
|
|
1625
|
+
|
|
1626
|
+
it("records the collapsed bubble impression, then the in-panel card on open", async () => {
|
|
1627
|
+
const { inspector, internals } = mount();
|
|
1628
|
+
|
|
1629
|
+
await internals.fetchAnnouncement();
|
|
1630
|
+
await inspector.updateComplete;
|
|
1631
|
+
|
|
1632
|
+
const first = eventsNamed("oss.inspector.banner_viewed");
|
|
1633
|
+
expect(first).toHaveLength(1);
|
|
1634
|
+
expect(first[0]!.properties).toMatchObject({
|
|
1635
|
+
banner_id: timestamp,
|
|
1636
|
+
surface: "collapsed_preview",
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// Opening reveals the in-panel card — a distinct impression.
|
|
1640
|
+
inspector.shadowRoot
|
|
1641
|
+
?.querySelector<HTMLElement>(".announcement-preview")
|
|
1642
|
+
?.click();
|
|
1643
|
+
await inspector.updateComplete;
|
|
1644
|
+
|
|
1645
|
+
const surfaces = eventsNamed("oss.inspector.banner_viewed").map(
|
|
1646
|
+
(post) => post.properties.surface,
|
|
1647
|
+
);
|
|
1648
|
+
expect(surfaces).toEqual(["collapsed_preview", "expanded_card"]);
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
it("does not re-record an impression for a surface already seen this mount", async () => {
|
|
1652
|
+
const { inspector, internals } = mount();
|
|
1653
|
+
|
|
1654
|
+
await internals.fetchAnnouncement();
|
|
1655
|
+
await internals.fetchAnnouncement();
|
|
1656
|
+
await inspector.updateComplete;
|
|
1657
|
+
|
|
1658
|
+
expect(eventsNamed("oss.inspector.banner_viewed")).toHaveLength(1);
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
it("emits opened with the floating-button source and announcement context", async () => {
|
|
1662
|
+
const { inspector, internals } = mount();
|
|
1663
|
+
|
|
1664
|
+
await internals.fetchAnnouncement();
|
|
1665
|
+
await inspector.updateComplete;
|
|
1666
|
+
|
|
1667
|
+
inspector.shadowRoot
|
|
1668
|
+
?.querySelector<HTMLElement>('button[aria-label="Web Inspector"]')
|
|
1669
|
+
?.click();
|
|
1670
|
+
await inspector.updateComplete;
|
|
1671
|
+
|
|
1672
|
+
const opened = eventsNamed("oss.inspector.opened");
|
|
1673
|
+
expect(opened).toHaveLength(1);
|
|
1674
|
+
expect(opened[0]!.properties).toMatchObject({
|
|
1675
|
+
open_source: "floating_button",
|
|
1676
|
+
has_unseen_announcement: true,
|
|
1677
|
+
package_name: "@copilotkit/web-inspector",
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
it("attributes an open through the announcement bubble to that surface", async () => {
|
|
1682
|
+
const { inspector, internals } = mount();
|
|
1683
|
+
|
|
1684
|
+
await internals.fetchAnnouncement();
|
|
1685
|
+
await inspector.updateComplete;
|
|
1686
|
+
|
|
1687
|
+
inspector.shadowRoot
|
|
1688
|
+
?.querySelector<HTMLElement>(".announcement-preview")
|
|
1689
|
+
?.click();
|
|
1690
|
+
await inspector.updateComplete;
|
|
1691
|
+
|
|
1692
|
+
expect(eventsNamed("oss.inspector.opened")[0]!.properties).toMatchObject({
|
|
1693
|
+
open_source: "announcement_preview",
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it("counts one open per open, and nothing for an already-open panel", async () => {
|
|
1698
|
+
const { inspector, internals } = mount();
|
|
1699
|
+
await inspector.updateComplete;
|
|
1700
|
+
|
|
1701
|
+
internals.openInspector("floating_button");
|
|
1702
|
+
internals.openInspector("floating_button");
|
|
1703
|
+
await inspector.updateComplete;
|
|
1704
|
+
|
|
1705
|
+
expect(eventsNamed("oss.inspector.opened")).toHaveLength(1);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it("does not count a restored-open panel as an open", async () => {
|
|
1709
|
+
const { inspector, internals } = mount();
|
|
1710
|
+
// Restoring persisted state assigns isOpen directly rather than calling
|
|
1711
|
+
// openInspector — otherwise every reload / dev-server hot reload would
|
|
1712
|
+
// register as a fresh open.
|
|
1713
|
+
internals.isOpen = true;
|
|
1714
|
+
inspector.requestUpdate();
|
|
1715
|
+
await inspector.updateComplete;
|
|
1716
|
+
|
|
1717
|
+
expect(eventsNamed("oss.inspector.opened")).toHaveLength(0);
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
// An open while disconnected is still a real open, so it is reported — but the
|
|
1721
|
+
// /info-derived dimensions are omitted rather than guessed. Recording the
|
|
1722
|
+
// `sse` default would permanently misattribute an Intelligence runtime.
|
|
1723
|
+
it("omits license and runtime mode for an open before the handshake", async () => {
|
|
1724
|
+
const { inspector, internals } = mount(false, false);
|
|
1725
|
+
|
|
1726
|
+
internals.openInspector("floating_button");
|
|
1727
|
+
await inspector.updateComplete;
|
|
1728
|
+
|
|
1729
|
+
const opened = eventsNamed("oss.inspector.opened");
|
|
1730
|
+
expect(opened).toHaveLength(1);
|
|
1731
|
+
expect(opened[0]!.properties).toMatchObject({
|
|
1732
|
+
open_source: "floating_button",
|
|
1733
|
+
// Config-derived, so accurate immediately.
|
|
1734
|
+
runtime_url_type: "localhost",
|
|
1735
|
+
});
|
|
1736
|
+
expect(opened[0]!.properties).not.toHaveProperty("runtime_mode");
|
|
1737
|
+
expect(opened[0]!.properties).not.toHaveProperty("license_status");
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it("records license and runtime mode for an open against a connected Intelligence runtime", async () => {
|
|
1741
|
+
const { inspector, harness, internals } = mount(false, false);
|
|
1742
|
+
harness.completeHandshake({
|
|
1743
|
+
telemetryDisabled: false,
|
|
1744
|
+
runtimeMode: "intelligence",
|
|
1745
|
+
licenseStatus: "valid",
|
|
1746
|
+
});
|
|
1747
|
+
await inspector.updateComplete;
|
|
1748
|
+
|
|
1749
|
+
internals.openInspector("floating_button");
|
|
1750
|
+
await inspector.updateComplete;
|
|
1751
|
+
|
|
1752
|
+
expect(eventsNamed("oss.inspector.opened")[0]!.properties).toMatchObject({
|
|
1753
|
+
open_source: "floating_button",
|
|
1754
|
+
runtime_mode: "intelligence",
|
|
1755
|
+
license_status: "valid",
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
// The impression deferral predates the surface split and must survive it: the
|
|
1760
|
+
// runtime's opt-out only arrives with /info.
|
|
1761
|
+
it("holds a banner impression until the handshake, then drops it when telemetry is disabled", async () => {
|
|
1762
|
+
const { inspector, harness, internals } = mount(false, false);
|
|
1763
|
+
|
|
1764
|
+
await internals.fetchAnnouncement();
|
|
1765
|
+
await inspector.updateComplete;
|
|
1766
|
+
expect(eventsNamed("oss.inspector.banner_viewed")).toHaveLength(0);
|
|
1767
|
+
|
|
1768
|
+
harness.completeHandshake({ telemetryDisabled: true });
|
|
1769
|
+
await inspector.updateComplete;
|
|
1770
|
+
|
|
1771
|
+
expect(eventsNamed("oss.inspector.banner_viewed")).toHaveLength(0);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
it("releases a held banner impression once the runtime allows telemetry", async () => {
|
|
1775
|
+
const { inspector, harness, internals } = mount(false, false);
|
|
1776
|
+
|
|
1777
|
+
await internals.fetchAnnouncement();
|
|
1778
|
+
await inspector.updateComplete;
|
|
1779
|
+
expect(eventsNamed("oss.inspector.banner_viewed")).toHaveLength(0);
|
|
1780
|
+
|
|
1781
|
+
harness.completeHandshake({ telemetryDisabled: false });
|
|
1782
|
+
await inspector.updateComplete;
|
|
1783
|
+
|
|
1784
|
+
const viewed = eventsNamed("oss.inspector.banner_viewed");
|
|
1785
|
+
expect(viewed).toHaveLength(1);
|
|
1786
|
+
expect(viewed[0]!.properties).toMatchObject({
|
|
1787
|
+
surface: "collapsed_preview",
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
it("emits nothing when the runtime has telemetry disabled", async () => {
|
|
1792
|
+
const { inspector, internals } = mount(true);
|
|
1793
|
+
|
|
1794
|
+
await internals.fetchAnnouncement();
|
|
1795
|
+
inspector.shadowRoot
|
|
1796
|
+
?.querySelector<HTMLElement>(".announcement-preview")
|
|
1797
|
+
?.click();
|
|
1798
|
+
await inspector.updateComplete;
|
|
1799
|
+
|
|
1800
|
+
expect(posts()).toEqual([]);
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1494
1804
|
// --- Owned thread store header forwarding (issue #5581) ---
|
|
1495
1805
|
//
|
|
1496
1806
|
// When useThreads() isn't mounted, the inspector creates its own thread store
|
|
@@ -1503,6 +1813,12 @@ type HeaderMockCore = {
|
|
|
1503
1813
|
context: Record<string, unknown>;
|
|
1504
1814
|
properties: Record<string, unknown>;
|
|
1505
1815
|
telemetryDisabled: boolean;
|
|
1816
|
+
// Mirrors CopilotKitCore's pre-handshake state (`agent-registry.ts:77`):
|
|
1817
|
+
// `runtimeMode` already reads its `sse` default and `licenseStatus` is absent,
|
|
1818
|
+
// both of which only become real once /info answers. Without this, telemetry
|
|
1819
|
+
// assertions about pre-handshake segmentation pass for the wrong reason.
|
|
1820
|
+
runtimeMode: string;
|
|
1821
|
+
licenseStatus?: string;
|
|
1506
1822
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
1507
1823
|
runtimeUrl: string;
|
|
1508
1824
|
headers: Record<string, string>;
|
|
@@ -1534,6 +1850,7 @@ function createHeaderMockCore(
|
|
|
1534
1850
|
context: {},
|
|
1535
1851
|
properties: {},
|
|
1536
1852
|
telemetryDisabled,
|
|
1853
|
+
runtimeMode: "sse",
|
|
1537
1854
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
1538
1855
|
runtimeUrl: "http://localhost/api",
|
|
1539
1856
|
headers,
|
|
@@ -1575,6 +1892,29 @@ function createHeaderMockCore(
|
|
|
1575
1892
|
s.onHeadersChanged?.({ copilotkit: asCore(), headers: nextHeaders }),
|
|
1576
1893
|
);
|
|
1577
1894
|
},
|
|
1895
|
+
/**
|
|
1896
|
+
* Simulates the /info handshake landing: applies what the runtime reported,
|
|
1897
|
+
* then transitions to `connected`. Lets a test observe what is sent after
|
|
1898
|
+
* the handshake versus before it — the segmentation fields do not exist
|
|
1899
|
+
* until this point.
|
|
1900
|
+
*/
|
|
1901
|
+
completeHandshake(
|
|
1902
|
+
reported: {
|
|
1903
|
+
telemetryDisabled?: boolean;
|
|
1904
|
+
licenseStatus?: string;
|
|
1905
|
+
runtimeMode?: string;
|
|
1906
|
+
} = {},
|
|
1907
|
+
) {
|
|
1908
|
+
Object.assign(core, reported);
|
|
1909
|
+
core.runtimeConnectionStatus =
|
|
1910
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
1911
|
+
subscribers.forEach((s) =>
|
|
1912
|
+
s.onRuntimeConnectionStatusChanged?.({
|
|
1913
|
+
copilotkit: asCore(),
|
|
1914
|
+
status: core.runtimeConnectionStatus,
|
|
1915
|
+
}),
|
|
1916
|
+
);
|
|
1917
|
+
},
|
|
1578
1918
|
};
|
|
1579
1919
|
}
|
|
1580
1920
|
|
package/src/index.ts
CHANGED
|
@@ -61,7 +61,9 @@ import {
|
|
|
61
61
|
getTelemetryDistinctIdForUrl,
|
|
62
62
|
maybeShowDisclosure,
|
|
63
63
|
trackBannerClicked,
|
|
64
|
+
trackBannerDismissed,
|
|
64
65
|
trackBannerViewed,
|
|
66
|
+
trackInspectorOpened,
|
|
65
67
|
trackTalkToEngineerClicked,
|
|
66
68
|
trackThreadsEmptyEnabledViewed,
|
|
67
69
|
trackThreadsEnabledViewed,
|
|
@@ -79,7 +81,9 @@ import {
|
|
|
79
81
|
trackThreadsTalkToEngineerClicked,
|
|
80
82
|
} from "./lib/telemetry.js";
|
|
81
83
|
import type {
|
|
84
|
+
BannerSurface,
|
|
82
85
|
InspectorMemoryTelemetryProps,
|
|
86
|
+
InspectorOpenSource,
|
|
83
87
|
InspectorThreadTelemetryProps,
|
|
84
88
|
} from "./lib/telemetry.js";
|
|
85
89
|
|
|
@@ -127,6 +131,9 @@ const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
|
|
|
127
131
|
const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 700 };
|
|
128
132
|
const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
|
|
129
133
|
const MAX_AGENT_EVENTS = 200;
|
|
134
|
+
// Cap on banner impressions held while waiting for the runtime handshake, so a
|
|
135
|
+
// runtime that never connects can't accumulate an unbounded queue.
|
|
136
|
+
const MAX_PENDING_BANNER_VIEWED = 20;
|
|
130
137
|
const MAX_TOTAL_EVENTS = 500;
|
|
131
138
|
const INTELLIGENCE_SIGNUP_URL = "https://go.copilotkit.ai/intelligence-signup";
|
|
132
139
|
const THREADS_INTELLIGENCE_SIGNIN_URL =
|
|
@@ -4556,16 +4563,27 @@ export class WebInspectorElement extends LitElement {
|
|
|
4556
4563
|
private announcementPromise: Promise<void> | null = null;
|
|
4557
4564
|
private showAnnouncementPreview = true;
|
|
4558
4565
|
private announcementExpanded = false;
|
|
4559
|
-
// Per-instance dedup for `oss.inspector.banner_viewed
|
|
4560
|
-
//
|
|
4561
|
-
//
|
|
4562
|
-
//
|
|
4563
|
-
//
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
+
// Per-instance dedup for `oss.inspector.banner_viewed`, keyed by
|
|
4567
|
+
// `${timestamp}:${surface}` so the event fires at most once per
|
|
4568
|
+
// announcement per surface per inspector mount. Plan calls for "de-dup per
|
|
4569
|
+
// timestamp per session"; instance-scoping is closer to per-mount than
|
|
4570
|
+
// per-tab (sessionStorage), but for the inspector the distinction is
|
|
4571
|
+
// academic — inspector instances rarely outlive the page. The surface is
|
|
4572
|
+
// part of the key (OSS-568) because the bubble on the collapsed widget and
|
|
4573
|
+
// the card inside the opened panel are separate impressions: a user can see
|
|
4574
|
+
// one without ever seeing the other.
|
|
4575
|
+
private viewedBannerSurfaces: Set<string> = new Set();
|
|
4576
|
+
// Impressions wait for the runtime handshake before going out: the runtime's
|
|
4577
|
+
// opt-out arrives in the /info response, and `telemetryDisabled` reads `false`
|
|
4578
|
+
// until then, so sending directly would post on a placeholder. This deferral
|
|
4579
|
+
// predates the surface split — preserved here, widened from a single slot to a
|
|
4580
|
+
// queue because opening the panel reveals the second surface and can happen
|
|
4581
|
+
// before the first impression has flushed.
|
|
4582
|
+
private pendingBannerViewed: Array<{
|
|
4566
4583
|
banner_id: string;
|
|
4584
|
+
surface: BannerSurface;
|
|
4567
4585
|
cta_label?: string;
|
|
4568
|
-
}
|
|
4586
|
+
}> = [];
|
|
4569
4587
|
// Per-instance dedup for `oss.inspector.banner_clicked` (keyed by
|
|
4570
4588
|
// `${bannerId}:${cta}`) so copy-button retries and accidental multi-clicks
|
|
4571
4589
|
// don't inflate funnel counts beyond one signal per intent type per banner.
|
|
@@ -7255,7 +7273,7 @@ ${argsString}</pre
|
|
|
7255
7273
|
!this.isOpen &&
|
|
7256
7274
|
!this.draggedDuringInteraction
|
|
7257
7275
|
) {
|
|
7258
|
-
this.openInspector();
|
|
7276
|
+
this.openInspector("floating_button");
|
|
7259
7277
|
}
|
|
7260
7278
|
|
|
7261
7279
|
this.resetPointerTracking();
|
|
@@ -7288,7 +7306,7 @@ ${argsString}</pre
|
|
|
7288
7306
|
|
|
7289
7307
|
if (!this.isOpen) {
|
|
7290
7308
|
event.preventDefault();
|
|
7291
|
-
this.openInspector();
|
|
7309
|
+
this.openInspector("floating_button");
|
|
7292
7310
|
}
|
|
7293
7311
|
};
|
|
7294
7312
|
|
|
@@ -7759,11 +7777,12 @@ ${argsString}</pre
|
|
|
7759
7777
|
this.draggedDuringInteraction = false;
|
|
7760
7778
|
}
|
|
7761
7779
|
|
|
7762
|
-
private openInspector(): void {
|
|
7780
|
+
private openInspector(source: InspectorOpenSource): void {
|
|
7763
7781
|
if (this.isOpen) {
|
|
7764
7782
|
return;
|
|
7765
7783
|
}
|
|
7766
7784
|
|
|
7785
|
+
const hadUnseenAnnouncement = this.hasUnseenAnnouncement;
|
|
7767
7786
|
this.showAnnouncementPreview = false; // hide the bubble once the inspector is opened
|
|
7768
7787
|
|
|
7769
7788
|
this.ensureAnnouncementLoading();
|
|
@@ -7771,6 +7790,12 @@ ${argsString}</pre
|
|
|
7771
7790
|
this.isOpen = true;
|
|
7772
7791
|
this.persistState(); // Save the open state
|
|
7773
7792
|
|
|
7793
|
+
this.trackOpened(source, hadUnseenAnnouncement);
|
|
7794
|
+
// The in-panel announcement card is now the visible surface, so it earns
|
|
7795
|
+
// its own banner_viewed impression (no-op when the announcement hasn't
|
|
7796
|
+
// loaded yet — fetchAnnouncement records it on arrival instead).
|
|
7797
|
+
this.maybeTrackBannerViewed();
|
|
7798
|
+
|
|
7774
7799
|
// Apply docking styles if in docked mode
|
|
7775
7800
|
if (this.dockMode !== "floating") {
|
|
7776
7801
|
this.applyDockStyles();
|
|
@@ -8290,6 +8315,52 @@ ${argsString}</pre
|
|
|
8290
8315
|
`;
|
|
8291
8316
|
}
|
|
8292
8317
|
|
|
8318
|
+
// Fires `oss.inspector.opened` for a user-initiated open (OSS-566).
|
|
8319
|
+
// Restoring a persisted-open panel on mount assigns `isOpen` directly
|
|
8320
|
+
// instead of routing through `openInspector`, so page reloads and dev-server
|
|
8321
|
+
// hot reloads are not counted as opens.
|
|
8322
|
+
private trackOpened(
|
|
8323
|
+
source: InspectorOpenSource,
|
|
8324
|
+
hadUnseenAnnouncement: boolean,
|
|
8325
|
+
): void {
|
|
8326
|
+
if (this.core?.telemetryDisabled) return;
|
|
8327
|
+
// `license_status` and `runtime_mode` come from /info. Before the handshake
|
|
8328
|
+
// `licenseStatus` is undefined and `runtimeMode` reads its `sse` default, so
|
|
8329
|
+
// recording them would permanently attribute an early open against an
|
|
8330
|
+
// Intelligence runtime to SSE. Omitted rather than guessed — an absent
|
|
8331
|
+
// dimension is honest, a wrong one is not. `runtime_url_type` is derived
|
|
8332
|
+
// from configuration, so it is accurate immediately.
|
|
8333
|
+
const handshakeComplete = this.runtimeStatus === "connected";
|
|
8334
|
+
trackInspectorOpened({
|
|
8335
|
+
open_source: source,
|
|
8336
|
+
...(handshakeComplete
|
|
8337
|
+
? {
|
|
8338
|
+
license_status: this.core?.licenseStatus ?? undefined,
|
|
8339
|
+
runtime_mode: this.core?.runtimeMode ?? undefined,
|
|
8340
|
+
}
|
|
8341
|
+
: {}),
|
|
8342
|
+
runtime_url_type: getRuntimeUrlType(this.core?.runtimeUrl),
|
|
8343
|
+
has_unseen_announcement: hadUnseenAnnouncement,
|
|
8344
|
+
});
|
|
8345
|
+
}
|
|
8346
|
+
|
|
8347
|
+
// Fires `banner_dismissed` at most once per `${bannerId}:${surface}` per
|
|
8348
|
+
// mount. Emitted alongside `banner_clicked { cta: "dismiss" }` rather than
|
|
8349
|
+
// replacing it, so dashboards reading the `cta` value keep working.
|
|
8350
|
+
private trackBannerDismissedOnce(surface: BannerSurface): void {
|
|
8351
|
+
if (this.core?.telemetryDisabled) return;
|
|
8352
|
+
const id = this.announcementTimestamp;
|
|
8353
|
+
if (!id) return;
|
|
8354
|
+
const key = `${id}:dismissed:${surface}`;
|
|
8355
|
+
if (this.clickedBannerIds.has(key)) return;
|
|
8356
|
+
this.clickedBannerIds.add(key);
|
|
8357
|
+
trackBannerDismissed({
|
|
8358
|
+
banner_id: id,
|
|
8359
|
+
surface,
|
|
8360
|
+
cta_label: this.announcementCtaLabel ?? undefined,
|
|
8361
|
+
});
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8293
8364
|
// Fires `banner_clicked` at most once per `${bannerId}:${cta}` per mount so
|
|
8294
8365
|
// copy-button retries and accidental multi-clicks don't inflate funnel counts.
|
|
8295
8366
|
private trackBannerClickedOnce(opts: { cta: "body" | "dismiss" }): void {
|
|
@@ -11332,7 +11403,7 @@ ${prettyEvent}</pre
|
|
|
11332
11403
|
<button
|
|
11333
11404
|
class="announcement-dismiss ml-auto"
|
|
11334
11405
|
type="button"
|
|
11335
|
-
@click=${this.
|
|
11406
|
+
@click=${() => this.dismissAnnouncement("expanded_card")}
|
|
11336
11407
|
aria-label="Dismiss announcement"
|
|
11337
11408
|
>
|
|
11338
11409
|
${this.renderIcon("X")}
|
|
@@ -11372,14 +11443,56 @@ ${prettyEvent}</pre
|
|
|
11372
11443
|
</div>`;
|
|
11373
11444
|
}
|
|
11374
11445
|
|
|
11446
|
+
/**
|
|
11447
|
+
* Which announcement surface is on screen right now, or null when the
|
|
11448
|
+
* announcement isn't visible. Mirrors the render gates in
|
|
11449
|
+
* `renderAnnouncementPreview` (bubble) and `renderAnnouncementBanner`
|
|
11450
|
+
* (in-panel card) — opening the panel clears `showAnnouncementPreview`, so
|
|
11451
|
+
* the two are mutually exclusive.
|
|
11452
|
+
*/
|
|
11453
|
+
private getVisibleBannerSurface(): BannerSurface | null {
|
|
11454
|
+
if (!this.hasUnseenAnnouncement) return null;
|
|
11455
|
+
if (this.isOpen) {
|
|
11456
|
+
return this.announcementHtml ? "expanded_card" : null;
|
|
11457
|
+
}
|
|
11458
|
+
return this.showAnnouncementPreview && this.announcementPreviewText
|
|
11459
|
+
? "collapsed_preview"
|
|
11460
|
+
: null;
|
|
11461
|
+
}
|
|
11462
|
+
|
|
11463
|
+
/**
|
|
11464
|
+
* Records a `banner_viewed` impression for whichever surface is currently
|
|
11465
|
+
* visible, once per announcement per surface.
|
|
11466
|
+
*/
|
|
11467
|
+
private maybeTrackBannerViewed(): void {
|
|
11468
|
+
const id = this.announcementTimestamp;
|
|
11469
|
+
if (!id) return;
|
|
11470
|
+
const surface = this.getVisibleBannerSurface();
|
|
11471
|
+
if (!surface) return;
|
|
11472
|
+
const key = `${id}:${surface}`;
|
|
11473
|
+
if (this.viewedBannerSurfaces.has(key)) return;
|
|
11474
|
+
if (this.pendingBannerViewed.length >= MAX_PENDING_BANNER_VIEWED) return;
|
|
11475
|
+
this.viewedBannerSurfaces.add(key);
|
|
11476
|
+
this.pendingBannerViewed.push({
|
|
11477
|
+
banner_id: id,
|
|
11478
|
+
surface,
|
|
11479
|
+
cta_label: this.announcementCtaLabel ?? undefined,
|
|
11480
|
+
});
|
|
11481
|
+
this.flushPendingBannerViewed();
|
|
11482
|
+
}
|
|
11483
|
+
|
|
11484
|
+
// Releases held impressions once /info has answered, or discards them when it
|
|
11485
|
+
// reports telemetry disabled.
|
|
11375
11486
|
private flushPendingBannerViewed(): void {
|
|
11376
|
-
if (
|
|
11377
|
-
|
|
11487
|
+
if (this.pendingBannerViewed.length === 0) return;
|
|
11488
|
+
if (this.core?.telemetryDisabled) {
|
|
11489
|
+
this.pendingBannerViewed = [];
|
|
11378
11490
|
return;
|
|
11379
11491
|
}
|
|
11380
11492
|
if (this.runtimeStatus !== "connected") return;
|
|
11381
|
-
|
|
11382
|
-
this.pendingBannerViewed =
|
|
11493
|
+
const queued = this.pendingBannerViewed;
|
|
11494
|
+
this.pendingBannerViewed = [];
|
|
11495
|
+
for (const props of queued) trackBannerViewed(props);
|
|
11383
11496
|
}
|
|
11384
11497
|
|
|
11385
11498
|
private ensureAnnouncementLoading(): void {
|
|
@@ -11429,7 +11542,7 @@ ${prettyEvent}</pre
|
|
|
11429
11542
|
|
|
11430
11543
|
private handleAnnouncementPreviewClick(): void {
|
|
11431
11544
|
this.showAnnouncementPreview = false;
|
|
11432
|
-
this.openInspector();
|
|
11545
|
+
this.openInspector("announcement_preview");
|
|
11433
11546
|
}
|
|
11434
11547
|
|
|
11435
11548
|
// Dismissing the preview bubble must PERSIST via markAnnouncementSeen(),
|
|
@@ -11442,13 +11555,14 @@ ${prettyEvent}</pre
|
|
|
11442
11555
|
// Don't let the dismiss bubble to the preview body, whose click opens the
|
|
11443
11556
|
// inspector.
|
|
11444
11557
|
event.stopPropagation();
|
|
11445
|
-
this.
|
|
11558
|
+
this.dismissAnnouncement("collapsed_preview");
|
|
11446
11559
|
};
|
|
11447
11560
|
|
|
11448
|
-
private
|
|
11561
|
+
private dismissAnnouncement(surface: BannerSurface): void {
|
|
11449
11562
|
this.trackBannerClickedOnce({ cta: "dismiss" });
|
|
11563
|
+
this.trackBannerDismissedOnce(surface);
|
|
11450
11564
|
this.markAnnouncementSeen();
|
|
11451
|
-
}
|
|
11565
|
+
}
|
|
11452
11566
|
|
|
11453
11567
|
private async fetchAnnouncement(): Promise<void> {
|
|
11454
11568
|
try {
|
|
@@ -11489,21 +11603,9 @@ ${prettyEvent}</pre
|
|
|
11489
11603
|
this.announcementHtml = await this.convertMarkdownToHtml(markdown);
|
|
11490
11604
|
this.announcementLoaded = true;
|
|
11491
11605
|
|
|
11492
|
-
// banner_viewed: gate on actual visibility and per-mount dedup
|
|
11493
|
-
//
|
|
11494
|
-
|
|
11495
|
-
// onRuntimeConnectionStatusChanged once the handshake completes.
|
|
11496
|
-
if (
|
|
11497
|
-
this.hasUnseenAnnouncement &&
|
|
11498
|
-
!this.viewedBannerTimestamps.has(timestamp)
|
|
11499
|
-
) {
|
|
11500
|
-
this.viewedBannerTimestamps.add(timestamp);
|
|
11501
|
-
this.pendingBannerViewed = {
|
|
11502
|
-
banner_id: timestamp,
|
|
11503
|
-
cta_label: ctaLabel ?? undefined,
|
|
11504
|
-
};
|
|
11505
|
-
this.flushPendingBannerViewed();
|
|
11506
|
-
}
|
|
11606
|
+
// banner_viewed: gate on actual visibility and per-mount dedup, and
|
|
11607
|
+
// stamp the surface the announcement is showing on right now.
|
|
11608
|
+
this.maybeTrackBannerViewed();
|
|
11507
11609
|
|
|
11508
11610
|
this.requestUpdate();
|
|
11509
11611
|
} catch (error) {
|