@copilotkit/web-inspector 1.68.3 → 1.69.0
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/README.md +28 -12
- package/dist/assets/inspector-logo-kite.cjs +12 -0
- package/dist/assets/inspector-logo-kite.cjs.map +1 -0
- package/dist/assets/inspector-logo-kite.mjs +6 -0
- package/dist/assets/inspector-logo-kite.mjs.map +1 -0
- package/dist/index.cjs +3197 -1473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -30
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +122 -30
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3198 -1474
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +3716 -1517
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/home-briefing.cjs +270 -0
- package/dist/lib/home-briefing.cjs.map +1 -0
- package/dist/lib/home-briefing.mjs +269 -0
- package/dist/lib/home-briefing.mjs.map +1 -0
- package/dist/lib/inspector-nav.cjs +58 -0
- package/dist/lib/inspector-nav.cjs.map +1 -0
- package/dist/lib/inspector-nav.mjs +53 -0
- package/dist/lib/inspector-nav.mjs.map +1 -0
- package/dist/lib/persistence.cjs +102 -0
- package/dist/lib/persistence.cjs.map +1 -1
- package/dist/lib/persistence.mjs +98 -1
- package/dist/lib/persistence.mjs.map +1 -1
- package/dist/lib/telemetry.cjs +47 -33
- package/dist/lib/telemetry.cjs.map +1 -1
- package/dist/lib/telemetry.d.cts +1 -1
- package/dist/lib/telemetry.d.cts.map +1 -1
- package/dist/lib/telemetry.d.mts +1 -1
- package/dist/lib/telemetry.d.mts.map +1 -1
- package/dist/lib/telemetry.mjs +43 -31
- package/dist/lib/telemetry.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/styles/generated.cjs +1 -1
- package/dist/styles/generated.cjs.map +1 -1
- package/dist/styles/generated.mjs +1 -1
- package/dist/styles/generated.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/assets/inspector-logo-icon.cjs +0 -12
- package/dist/assets/inspector-logo-icon.cjs.map +0 -1
- package/dist/assets/inspector-logo-icon.mjs +0 -6
- package/dist/assets/inspector-logo-icon.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspector-nav.cjs","names":[],"sources":["../../src/lib/inspector-nav.ts"],"sourcesContent":["import type { InspectorLeafKey } from \"./telemetry.js\";\n\nexport type ShellGroupKey = \"home\" | \"workbench\" | \"inspect\";\n\nexport const INSPECTOR_GROUPS = {\n home: [\"home\", \"whats-new\"],\n workbench: [\"playground\", \"threads\", \"memories\"],\n inspect: [\n \"agents\",\n \"ag-ui-events\",\n \"frontend-tools\",\n \"capabilities\",\n \"agent-context\",\n ],\n} as const satisfies Record<ShellGroupKey, readonly InspectorLeafKey[]>;\n\nexport type InspectorNavGroupKey = keyof typeof INSPECTOR_GROUPS;\nexport type MenuKey = (typeof INSPECTOR_GROUPS)[InspectorNavGroupKey][number];\n\nexport const INSPECTOR_MENU_KEYS: ReadonlyArray<MenuKey> = [\n ...INSPECTOR_GROUPS.home,\n ...INSPECTOR_GROUPS.workbench,\n ...INSPECTOR_GROUPS.inspect,\n];\n\nexport const INSPECTOR_NAV_SECTIONS: ReadonlyArray<{\n group: InspectorNavGroupKey;\n label: string | null;\n}> = [\n { group: \"home\", label: null },\n { group: \"workbench\", label: \"Workbench\" },\n { group: \"inspect\", label: \"Inspect\" },\n];\n\nexport const ICON_RAIL_MAX_WIDTH_PX = 720;\n\n/** Return whether persisted state names a known Inspector leaf. */\nexport function isInspectorMenuKey(value: unknown): value is MenuKey {\n return (\n typeof value === \"string\" &&\n INSPECTOR_MENU_KEYS.some((menuKey) => menuKey === value)\n );\n}\n\n/** Return the sidebar group that owns a leaf. */\nexport function getGroupForMenu(key: MenuKey): InspectorNavGroupKey {\n for (const group of Object.keys(INSPECTOR_GROUPS) as InspectorNavGroupKey[]) {\n if (INSPECTOR_GROUPS[group].some((menuKey) => menuKey === key)) {\n return group;\n }\n }\n\n return \"workbench\";\n}\n\n/** Return whether the sidebar should collapse to an icon rail. */\nexport function shouldUseIconRail(args: {\n dockedLeft: boolean;\n width: number;\n}): boolean {\n return args.dockedLeft || args.width < ICON_RAIL_MAX_WIDTH_PX;\n}\n"],"mappings":";;AAIA,MAAa,mBAAmB;CAC9B,MAAM,CAAC,QAAQ,YAAY;CAC3B,WAAW;EAAC;EAAc;EAAW;EAAW;CAChD,SAAS;EACP;EACA;EACA;EACA;EACA;EACD;CACF;AAKD,MAAa,sBAA8C;CACzD,GAAG,iBAAiB;CACpB,GAAG,iBAAiB;CACpB,GAAG,iBAAiB;CACrB;AAED,MAAa,yBAGR;CACH;EAAE,OAAO;EAAQ,OAAO;EAAM;CAC9B;EAAE,OAAO;EAAa,OAAO;EAAa;CAC1C;EAAE,OAAO;EAAW,OAAO;EAAW;CACvC;AAED,MAAa,yBAAyB;;AAGtC,SAAgB,mBAAmB,OAAkC;AACnE,QACE,OAAO,UAAU,YACjB,oBAAoB,MAAM,YAAY,YAAY,MAAM;;;AAK5D,SAAgB,gBAAgB,KAAoC;AAClE,MAAK,MAAM,SAAS,OAAO,KAAK,iBAAiB,CAC/C,KAAI,iBAAiB,OAAO,MAAM,YAAY,YAAY,IAAI,CAC5D,QAAO;AAIX,QAAO;;;AAIT,SAAgB,kBAAkB,MAGtB;AACV,QAAO,KAAK,cAAc,KAAK,QAAQ"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/lib/inspector-nav.ts
|
|
2
|
+
const INSPECTOR_GROUPS = {
|
|
3
|
+
home: ["home", "whats-new"],
|
|
4
|
+
workbench: [
|
|
5
|
+
"playground",
|
|
6
|
+
"threads",
|
|
7
|
+
"memories"
|
|
8
|
+
],
|
|
9
|
+
inspect: [
|
|
10
|
+
"agents",
|
|
11
|
+
"ag-ui-events",
|
|
12
|
+
"frontend-tools",
|
|
13
|
+
"capabilities",
|
|
14
|
+
"agent-context"
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
const INSPECTOR_MENU_KEYS = [
|
|
18
|
+
...INSPECTOR_GROUPS.home,
|
|
19
|
+
...INSPECTOR_GROUPS.workbench,
|
|
20
|
+
...INSPECTOR_GROUPS.inspect
|
|
21
|
+
];
|
|
22
|
+
const INSPECTOR_NAV_SECTIONS = [
|
|
23
|
+
{
|
|
24
|
+
group: "home",
|
|
25
|
+
label: null
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
group: "workbench",
|
|
29
|
+
label: "Workbench"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
group: "inspect",
|
|
33
|
+
label: "Inspect"
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
const ICON_RAIL_MAX_WIDTH_PX = 720;
|
|
37
|
+
/** Return whether persisted state names a known Inspector leaf. */
|
|
38
|
+
function isInspectorMenuKey(value) {
|
|
39
|
+
return typeof value === "string" && INSPECTOR_MENU_KEYS.some((menuKey) => menuKey === value);
|
|
40
|
+
}
|
|
41
|
+
/** Return the sidebar group that owns a leaf. */
|
|
42
|
+
function getGroupForMenu(key) {
|
|
43
|
+
for (const group of Object.keys(INSPECTOR_GROUPS)) if (INSPECTOR_GROUPS[group].some((menuKey) => menuKey === key)) return group;
|
|
44
|
+
return "workbench";
|
|
45
|
+
}
|
|
46
|
+
/** Return whether the sidebar should collapse to an icon rail. */
|
|
47
|
+
function shouldUseIconRail(args) {
|
|
48
|
+
return args.dockedLeft || args.width < ICON_RAIL_MAX_WIDTH_PX;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { INSPECTOR_GROUPS, INSPECTOR_NAV_SECTIONS, getGroupForMenu, isInspectorMenuKey, shouldUseIconRail };
|
|
53
|
+
//# sourceMappingURL=inspector-nav.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspector-nav.mjs","names":[],"sources":["../../src/lib/inspector-nav.ts"],"sourcesContent":["import type { InspectorLeafKey } from \"./telemetry.js\";\n\nexport type ShellGroupKey = \"home\" | \"workbench\" | \"inspect\";\n\nexport const INSPECTOR_GROUPS = {\n home: [\"home\", \"whats-new\"],\n workbench: [\"playground\", \"threads\", \"memories\"],\n inspect: [\n \"agents\",\n \"ag-ui-events\",\n \"frontend-tools\",\n \"capabilities\",\n \"agent-context\",\n ],\n} as const satisfies Record<ShellGroupKey, readonly InspectorLeafKey[]>;\n\nexport type InspectorNavGroupKey = keyof typeof INSPECTOR_GROUPS;\nexport type MenuKey = (typeof INSPECTOR_GROUPS)[InspectorNavGroupKey][number];\n\nexport const INSPECTOR_MENU_KEYS: ReadonlyArray<MenuKey> = [\n ...INSPECTOR_GROUPS.home,\n ...INSPECTOR_GROUPS.workbench,\n ...INSPECTOR_GROUPS.inspect,\n];\n\nexport const INSPECTOR_NAV_SECTIONS: ReadonlyArray<{\n group: InspectorNavGroupKey;\n label: string | null;\n}> = [\n { group: \"home\", label: null },\n { group: \"workbench\", label: \"Workbench\" },\n { group: \"inspect\", label: \"Inspect\" },\n];\n\nexport const ICON_RAIL_MAX_WIDTH_PX = 720;\n\n/** Return whether persisted state names a known Inspector leaf. */\nexport function isInspectorMenuKey(value: unknown): value is MenuKey {\n return (\n typeof value === \"string\" &&\n INSPECTOR_MENU_KEYS.some((menuKey) => menuKey === value)\n );\n}\n\n/** Return the sidebar group that owns a leaf. */\nexport function getGroupForMenu(key: MenuKey): InspectorNavGroupKey {\n for (const group of Object.keys(INSPECTOR_GROUPS) as InspectorNavGroupKey[]) {\n if (INSPECTOR_GROUPS[group].some((menuKey) => menuKey === key)) {\n return group;\n }\n }\n\n return \"workbench\";\n}\n\n/** Return whether the sidebar should collapse to an icon rail. */\nexport function shouldUseIconRail(args: {\n dockedLeft: boolean;\n width: number;\n}): boolean {\n return args.dockedLeft || args.width < ICON_RAIL_MAX_WIDTH_PX;\n}\n"],"mappings":";AAIA,MAAa,mBAAmB;CAC9B,MAAM,CAAC,QAAQ,YAAY;CAC3B,WAAW;EAAC;EAAc;EAAW;EAAW;CAChD,SAAS;EACP;EACA;EACA;EACA;EACA;EACD;CACF;AAKD,MAAa,sBAA8C;CACzD,GAAG,iBAAiB;CACpB,GAAG,iBAAiB;CACpB,GAAG,iBAAiB;CACrB;AAED,MAAa,yBAGR;CACH;EAAE,OAAO;EAAQ,OAAO;EAAM;CAC9B;EAAE,OAAO;EAAa,OAAO;EAAa;CAC1C;EAAE,OAAO;EAAW,OAAO;EAAW;CACvC;AAED,MAAa,yBAAyB;;AAGtC,SAAgB,mBAAmB,OAAkC;AACnE,QACE,OAAO,UAAU,YACjB,oBAAoB,MAAM,YAAY,YAAY,MAAM;;;AAK5D,SAAgB,gBAAgB,KAAoC;AAClE,MAAK,MAAM,SAAS,OAAO,KAAK,iBAAiB,CAC/C,KAAI,iBAAiB,OAAO,MAAM,YAAY,YAAY,IAAI,CAC5D,QAAO;AAIX,QAAO;;;AAIT,SAAgB,kBAAkB,MAGtB;AACV,QAAO,KAAK,cAAc,KAAK,QAAQ"}
|
package/dist/lib/persistence.cjs
CHANGED
|
@@ -51,6 +51,103 @@ function isFiniteNumber(value) {
|
|
|
51
51
|
function isValidDockMode(value) {
|
|
52
52
|
return value === "floating" || value === "docked-left";
|
|
53
53
|
}
|
|
54
|
+
const ANNOUNCEMENT_READ_COOKIE_NAME = "cpk_inspector_announcements";
|
|
55
|
+
const ANNOUNCEMENT_READ_MIRROR_KEY = "cpk:inspector:announcement_read";
|
|
56
|
+
const LEGACY_ANNOUNCEMENT_READ_KEY = "cpk:inspector:announcements";
|
|
57
|
+
const ANNOUNCEMENT_PULSED_SESSION_KEY = "cpk:inspector:pulsed";
|
|
58
|
+
const ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 3600 * 24 * 365;
|
|
59
|
+
/**
|
|
60
|
+
* The announcement timestamp the user has already read, or `null` when
|
|
61
|
+
* nothing has been read yet. Prefers the host-scoped cookie and falls back to
|
|
62
|
+
* the origin-scoped mirror, so cookie-blocking browsers still remember the
|
|
63
|
+
* read within the port they are on.
|
|
64
|
+
*/
|
|
65
|
+
function loadAnnouncementReadTimestamp() {
|
|
66
|
+
return parseTimestampPayload(readAnnouncementCookie()) ?? parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Records an announcement as read in both the host-scoped cookie and the
|
|
70
|
+
* origin-scoped mirror. The stored value is `{"timestamp":"…"}` — the same
|
|
71
|
+
* shape the announcement state has always used.
|
|
72
|
+
*/
|
|
73
|
+
function saveAnnouncementReadTimestamp(timestamp) {
|
|
74
|
+
const payload = JSON.stringify({ timestamp });
|
|
75
|
+
writeAnnouncementCookie(payload);
|
|
76
|
+
writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Deletes the superseded origin-scoped read state. Safe to call on every
|
|
80
|
+
* startup: nothing writes that key any more, so the value cannot come back.
|
|
81
|
+
*/
|
|
82
|
+
function clearLegacyAnnouncementReadState() {
|
|
83
|
+
removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The announcement timestamp this browser tab has already pulsed for, or
|
|
87
|
+
* `null` when it hasn't pulsed yet.
|
|
88
|
+
*/
|
|
89
|
+
function loadAnnouncementPulsedTimestamp() {
|
|
90
|
+
if (typeof window === "undefined") return null;
|
|
91
|
+
try {
|
|
92
|
+
return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Suppresses further pulses for this announcement in this browser tab. */
|
|
98
|
+
function saveAnnouncementPulsedTimestamp(timestamp) {
|
|
99
|
+
if (typeof window === "undefined") return;
|
|
100
|
+
try {
|
|
101
|
+
window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
function parseTimestampPayload(raw) {
|
|
105
|
+
if (!raw) return null;
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw);
|
|
108
|
+
return typeof parsed?.timestamp === "string" ? parsed.timestamp : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function readAnnouncementCookie() {
|
|
114
|
+
if (typeof document === "undefined") return null;
|
|
115
|
+
try {
|
|
116
|
+
for (const entry of document.cookie.split(";")) {
|
|
117
|
+
const separator = entry.indexOf("=");
|
|
118
|
+
if (separator === -1) continue;
|
|
119
|
+
if (entry.slice(0, separator).trim() !== ANNOUNCEMENT_READ_COOKIE_NAME) continue;
|
|
120
|
+
return decodeURIComponent(entry.slice(separator + 1).trim());
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function writeAnnouncementCookie(value) {
|
|
126
|
+
if (typeof document === "undefined") return;
|
|
127
|
+
try {
|
|
128
|
+
document.cookie = `${ANNOUNCEMENT_READ_COOKIE_NAME}=${encodeURIComponent(value)}; Path=/; Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
function readLocalStorageItem(key) {
|
|
132
|
+
if (typeof window === "undefined") return null;
|
|
133
|
+
try {
|
|
134
|
+
return window.localStorage.getItem(key);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function writeLocalStorageItem(key, value) {
|
|
140
|
+
if (typeof window === "undefined") return;
|
|
141
|
+
try {
|
|
142
|
+
window.localStorage.setItem(key, value);
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
function removeLocalStorageItem(key) {
|
|
146
|
+
if (typeof window === "undefined") return;
|
|
147
|
+
try {
|
|
148
|
+
window.localStorage.removeItem(key);
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
54
151
|
const TELEMETRY_DISTINCT_ID_KEY = "cpk:inspector:telemetry:distinct_id";
|
|
55
152
|
const TELEMETRY_OPT_OUT_KEY = "cpk:inspector:telemetry:opt_out";
|
|
56
153
|
const TELEMETRY_DISCLOSURE_SHOWN_KEY = "cpk:inspector:telemetry:disclosure_shown";
|
|
@@ -98,6 +195,7 @@ function generateUuidV4() {
|
|
|
98
195
|
}
|
|
99
196
|
|
|
100
197
|
//#endregion
|
|
198
|
+
exports.clearLegacyAnnouncementReadState = clearLegacyAnnouncementReadState;
|
|
101
199
|
exports.getOrCreateTelemetryDistinctId = getOrCreateTelemetryDistinctId;
|
|
102
200
|
exports.hasTelemetryDisclosureBeenShown = hasTelemetryDisclosureBeenShown;
|
|
103
201
|
exports.isTelemetryOptedOut = isTelemetryOptedOut;
|
|
@@ -105,7 +203,11 @@ exports.isValidAnchor = isValidAnchor;
|
|
|
105
203
|
exports.isValidDockMode = isValidDockMode;
|
|
106
204
|
exports.isValidPosition = isValidPosition;
|
|
107
205
|
exports.isValidSize = isValidSize;
|
|
206
|
+
exports.loadAnnouncementPulsedTimestamp = loadAnnouncementPulsedTimestamp;
|
|
207
|
+
exports.loadAnnouncementReadTimestamp = loadAnnouncementReadTimestamp;
|
|
108
208
|
exports.loadInspectorState = loadInspectorState;
|
|
109
209
|
exports.markTelemetryDisclosureShown = markTelemetryDisclosureShown;
|
|
210
|
+
exports.saveAnnouncementPulsedTimestamp = saveAnnouncementPulsedTimestamp;
|
|
211
|
+
exports.saveAnnouncementReadTimestamp = saveAnnouncementReadTimestamp;
|
|
110
212
|
exports.saveInspectorState = saveInspectorState;
|
|
111
213
|
//# sourceMappingURL=persistence.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.cjs","names":[],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n};\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that banner_viewed and banner_clicked from\n// the same page-load share one distinct_id even without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n"],"mappings":";;AAkBA,SAAgB,mBAAmB,YAA2C;AAC5E,KAAI,OAAO,WAAW,YACpB,QAAO;CAGT,MAAM,MAAM,OAAO,aAAa,QAAQ,WAAW;AACnD,KAAI,IACF,KAAI;EACF,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,UAAU,OAAO,WAAW,SAC9B,QAAO;SAEH;AAMV,KAAI,OAAO,aAAa,aAAa;EACnC,MAAM,SAAS,GAAG,WAAW;EAC7B,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,WAAW,OAAO,WAAW,OAAO,CAAC;AAC9C,MAAI,OAAO;GACT,MAAM,YAAY,MAAM,UAAU,OAAO,OAAO;AAChD,OAAI;IACF,MAAM,SAAS,KAAK,MAAM,mBAAmB,UAAU,CAAC;AACxD,QAAI,UAAU,OAAO,WAAW,SAC9B,QAAO;WAEH;AACN,WAAO;;;;AAKb,QAAO;;AAGT,SAAgB,mBACd,YACA,OACM;AACN,KAAI,OAAO,WAAW,YACpB;AAGF,KAAI;AACF,SAAO,aAAa,QAAQ,YAAY,KAAK,UAAU,MAAM,CAAC;UACvD,OAAO;AACd,UAAQ,KAAK,qCAAqC,MAAM;;;AAI5D,SAAgB,cAAc,OAAiC;AAC7D,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;CAGT,MAAM,YAAY;AAClB,SACG,UAAU,eAAe,UAAU,UAAU,eAAe,aAC5D,UAAU,aAAa,SAAS,UAAU,aAAa;;AAI5D,SAAgB,gBAAgB,OAAmC;AACjE,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;CAGT,MAAM,YAAY;AAClB,QAAO,eAAe,UAAU,EAAE,IAAI,eAAe,UAAU,EAAE;;AAGnE,SAAgB,YAAY,OAA+B;AACzD,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;CAGT,MAAM,YAAY;AAClB,QAAO,eAAe,UAAU,MAAM,IAAI,eAAe,UAAU,OAAO;;AAG5E,SAAgB,eAAe,OAAiC;AAC9D,QAAO,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM;;AAG5D,SAAgB,gBAAgB,OAAmC;AACjE,QAAO,UAAU,cAAc,UAAU;;AAO3C,MAAM,4BAA4B;AAClC,MAAM,wBAAwB;AAC9B,MAAM,iCACJ;AAMF,IAAI,qBAAoC;AAExC,SAAgB,iCAAyC;AACvD,KAAI,OAAO,WAAW,YAGpB,QAAO,gBAAgB;AAGzB,KAAI;EACF,MAAM,WAAW,OAAO,aAAa,QAAQ,0BAA0B;AACvE,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;EAC5C,MAAM,QAAQ,gBAAgB;AAC9B,SAAO,aAAa,QAAQ,2BAA2B,MAAM;AAC7D,SAAO;SACD;AACN,SAAQ,uBAAuB,gBAAgB;;;AASnD,SAAgB,sBAA+B;AAC7C,KAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,KAAI;AACF,SAAO,OAAO,aAAa,QAAQ,sBAAsB,KAAK;SACxD;AACN,SAAO;;;AAiBX,SAAgB,kCAA2C;AACzD,KAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,KAAI;AACF,SACE,OAAO,aAAa,QAAQ,+BAA+B,KAAK;SAE5D;AACN,SAAO;;;AAIX,SAAgB,+BAAqC;AACnD,KAAI,OAAO,WAAW,YAAa;AACnC,KAAI;AACF,SAAO,aAAa,QAAQ,gCAAgC,OAAO;SAC7D;;AAKV,SAAS,iBAAyB;AAChC,KACE,OAAO,WAAW,WAAW,eAC7B,OAAO,WAAW,OAAO,eAAe,WAExC,QAAO,WAAW,OAAO,YAAY;AAKvC,QAAO,uCAAuC,QAAQ,UAAU,MAAM;EACpE,MAAM,IAAK,KAAK,QAAQ,GAAG,KAAM;AAEjC,UADU,MAAM,MAAM,IAAK,IAAI,IAAO,GAC7B,SAAS,GAAG;GACrB"}
|
|
1
|
+
{"version":3,"file":"persistence.cjs","names":[],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n hasOpenedInspector?: boolean;\n sidebarCollapsed?: boolean;\n /** @deprecated Replaced by colorSchemePreference to distinguish a user choice from the old light default. */\n colorScheme?: \"light\" | \"dark\";\n colorSchemePreference?: \"light\" | \"dark\";\n};\n\nexport const HOME_NEWS_READ_STORAGE_KEY = \"cpk:inspector:home-news-read\";\n\n/** Return story ids the user has already opened or hovered on Home. */\nexport function loadHomeNewsReadIds(storageKey: string): string[] {\n if (typeof window === \"undefined\") {\n return [];\n }\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter((id): id is string => typeof id === \"string\");\n } catch {\n return [];\n }\n}\n\n/** Persist Home news story ids that the user has already seen. */\nexport function saveHomeNewsReadIds(storageKey: string, ids: string[]): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(ids));\n } catch (error) {\n console.warn(\"Failed to persist Home news read state\", error);\n }\n}\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Announcement read state — \"have I read this announcement\" is a property of\n// the person, not of the project, so it must survive a change of localhost\n// port. localStorage cannot express that: it is partitioned by origin and\n// origin includes the port, so :3000 and :5173 are separate stores. Cookies\n// are partitioned by host, so a cookie set on `localhost` without a `domain`\n// attribute is shared by every port on that host.\n//\n// Underscores, not colons: `:` is a separator in RFC 6265 and is not valid in\n// a cookie name. Browsers are lenient about it; we don't rely on that.\nconst ANNOUNCEMENT_READ_COOKIE_NAME = \"cpk_inspector_announcements\";\n\n// localStorage mirror of the cookie, so a browser that blocks cookies\n// degrades to per-port behaviour instead of losing the read state entirely.\n// A NEW key on purpose — the legacy one is abandoned, not migrated.\nconst ANNOUNCEMENT_READ_MIRROR_KEY = \"cpk:inspector:announcement_read\";\n\n// The superseded key. Every existing user is re-armed exactly once so they\n// discover the surface that replaced the announcement bubble, and the key is\n// deleted rather than left in place so nothing can fall back to it later.\nconst LEGACY_ANNOUNCEMENT_READ_KEY = \"cpk:inspector:announcements\";\n\n// Pulse suppression is per browser tab, and stores the announcement timestamp\n// rather than a boolean: a boolean would swallow a newly published\n// announcement for the rest of that tab's life, and the feed is fetched once\n// per mount with no polling.\nconst ANNOUNCEMENT_PULSED_SESSION_KEY = \"cpk:inspector:pulsed\";\n\n// Roughly one year. No `Secure` — local development is served over plain\n// HTTP — and no `HttpOnly`, because the component reads the value from\n// script. `Path=/` and `SameSite=Lax` keep it host-wide and same-site only.\nconst ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;\n\n/**\n * The announcement timestamp the user has already read, or `null` when\n * nothing has been read yet. Prefers the host-scoped cookie and falls back to\n * the origin-scoped mirror, so cookie-blocking browsers still remember the\n * read within the port they are on.\n */\nexport function loadAnnouncementReadTimestamp(): string | null {\n return (\n parseTimestampPayload(readAnnouncementCookie()) ??\n parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY))\n );\n}\n\n/**\n * Records an announcement as read in both the host-scoped cookie and the\n * origin-scoped mirror. The stored value is `{\"timestamp\":\"…\"}` — the same\n * shape the announcement state has always used.\n */\nexport function saveAnnouncementReadTimestamp(timestamp: string): void {\n const payload = JSON.stringify({ timestamp });\n writeAnnouncementCookie(payload);\n writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);\n}\n\n/**\n * Deletes the superseded origin-scoped read state. Safe to call on every\n * startup: nothing writes that key any more, so the value cannot come back.\n */\nexport function clearLegacyAnnouncementReadState(): void {\n removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);\n}\n\n/**\n * The announcement timestamp this browser tab has already pulsed for, or\n * `null` when it hasn't pulsed yet.\n */\nexport function loadAnnouncementPulsedTimestamp(): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);\n } catch {\n return null;\n }\n}\n\n/** Suppresses further pulses for this announcement in this browser tab. */\nexport function saveAnnouncementPulsedTimestamp(timestamp: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);\n } catch {\n // No-op — a lost suppression costs one extra pulse, never correctness.\n }\n}\n\nfunction parseTimestampPayload(raw: string | null): string | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as { timestamp?: unknown };\n return typeof parsed?.timestamp === \"string\" ? parsed.timestamp : null;\n } catch {\n return null;\n }\n}\n\nfunction readAnnouncementCookie(): string | null {\n if (typeof document === \"undefined\") return null;\n try {\n for (const entry of document.cookie.split(\";\")) {\n const separator = entry.indexOf(\"=\");\n if (separator === -1) continue;\n if (entry.slice(0, separator).trim() !== ANNOUNCEMENT_READ_COOKIE_NAME) {\n continue;\n }\n return decodeURIComponent(entry.slice(separator + 1).trim());\n }\n } catch {\n // Cookie access throws in sandboxed documents — fall through to the\n // mirror rather than breaking the host app.\n }\n return null;\n}\n\nfunction writeAnnouncementCookie(value: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${ANNOUNCEMENT_READ_COOKIE_NAME}=${encodeURIComponent(\n value,\n )}; Path=/; Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n } catch {\n // No-op — the mirror below is what a cookie-blocking browser falls back\n // to, and neither may break the host app.\n }\n}\n\nfunction readLocalStorageItem(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorageItem(key: string, value: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nfunction removeLocalStorageItem(key: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // No-op.\n }\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that whats_new_viewed and\n// whats_new_clicked from the same page-load share one distinct_id even\n// without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n"],"mappings":"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"}
|
package/dist/lib/persistence.mjs
CHANGED
|
@@ -50,6 +50,103 @@ function isFiniteNumber(value) {
|
|
|
50
50
|
function isValidDockMode(value) {
|
|
51
51
|
return value === "floating" || value === "docked-left";
|
|
52
52
|
}
|
|
53
|
+
const ANNOUNCEMENT_READ_COOKIE_NAME = "cpk_inspector_announcements";
|
|
54
|
+
const ANNOUNCEMENT_READ_MIRROR_KEY = "cpk:inspector:announcement_read";
|
|
55
|
+
const LEGACY_ANNOUNCEMENT_READ_KEY = "cpk:inspector:announcements";
|
|
56
|
+
const ANNOUNCEMENT_PULSED_SESSION_KEY = "cpk:inspector:pulsed";
|
|
57
|
+
const ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 3600 * 24 * 365;
|
|
58
|
+
/**
|
|
59
|
+
* The announcement timestamp the user has already read, or `null` when
|
|
60
|
+
* nothing has been read yet. Prefers the host-scoped cookie and falls back to
|
|
61
|
+
* the origin-scoped mirror, so cookie-blocking browsers still remember the
|
|
62
|
+
* read within the port they are on.
|
|
63
|
+
*/
|
|
64
|
+
function loadAnnouncementReadTimestamp() {
|
|
65
|
+
return parseTimestampPayload(readAnnouncementCookie()) ?? parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Records an announcement as read in both the host-scoped cookie and the
|
|
69
|
+
* origin-scoped mirror. The stored value is `{"timestamp":"…"}` — the same
|
|
70
|
+
* shape the announcement state has always used.
|
|
71
|
+
*/
|
|
72
|
+
function saveAnnouncementReadTimestamp(timestamp) {
|
|
73
|
+
const payload = JSON.stringify({ timestamp });
|
|
74
|
+
writeAnnouncementCookie(payload);
|
|
75
|
+
writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Deletes the superseded origin-scoped read state. Safe to call on every
|
|
79
|
+
* startup: nothing writes that key any more, so the value cannot come back.
|
|
80
|
+
*/
|
|
81
|
+
function clearLegacyAnnouncementReadState() {
|
|
82
|
+
removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The announcement timestamp this browser tab has already pulsed for, or
|
|
86
|
+
* `null` when it hasn't pulsed yet.
|
|
87
|
+
*/
|
|
88
|
+
function loadAnnouncementPulsedTimestamp() {
|
|
89
|
+
if (typeof window === "undefined") return null;
|
|
90
|
+
try {
|
|
91
|
+
return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Suppresses further pulses for this announcement in this browser tab. */
|
|
97
|
+
function saveAnnouncementPulsedTimestamp(timestamp) {
|
|
98
|
+
if (typeof window === "undefined") return;
|
|
99
|
+
try {
|
|
100
|
+
window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
function parseTimestampPayload(raw) {
|
|
104
|
+
if (!raw) return null;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
return typeof parsed?.timestamp === "string" ? parsed.timestamp : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readAnnouncementCookie() {
|
|
113
|
+
if (typeof document === "undefined") return null;
|
|
114
|
+
try {
|
|
115
|
+
for (const entry of document.cookie.split(";")) {
|
|
116
|
+
const separator = entry.indexOf("=");
|
|
117
|
+
if (separator === -1) continue;
|
|
118
|
+
if (entry.slice(0, separator).trim() !== ANNOUNCEMENT_READ_COOKIE_NAME) continue;
|
|
119
|
+
return decodeURIComponent(entry.slice(separator + 1).trim());
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function writeAnnouncementCookie(value) {
|
|
125
|
+
if (typeof document === "undefined") return;
|
|
126
|
+
try {
|
|
127
|
+
document.cookie = `${ANNOUNCEMENT_READ_COOKIE_NAME}=${encodeURIComponent(value)}; Path=/; Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
function readLocalStorageItem(key) {
|
|
131
|
+
if (typeof window === "undefined") return null;
|
|
132
|
+
try {
|
|
133
|
+
return window.localStorage.getItem(key);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function writeLocalStorageItem(key, value) {
|
|
139
|
+
if (typeof window === "undefined") return;
|
|
140
|
+
try {
|
|
141
|
+
window.localStorage.setItem(key, value);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
function removeLocalStorageItem(key) {
|
|
145
|
+
if (typeof window === "undefined") return;
|
|
146
|
+
try {
|
|
147
|
+
window.localStorage.removeItem(key);
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
53
150
|
const TELEMETRY_DISTINCT_ID_KEY = "cpk:inspector:telemetry:distinct_id";
|
|
54
151
|
const TELEMETRY_OPT_OUT_KEY = "cpk:inspector:telemetry:opt_out";
|
|
55
152
|
const TELEMETRY_DISCLOSURE_SHOWN_KEY = "cpk:inspector:telemetry:disclosure_shown";
|
|
@@ -97,5 +194,5 @@ function generateUuidV4() {
|
|
|
97
194
|
}
|
|
98
195
|
|
|
99
196
|
//#endregion
|
|
100
|
-
export { getOrCreateTelemetryDistinctId, hasTelemetryDisclosureBeenShown, isTelemetryOptedOut, isValidAnchor, isValidDockMode, isValidPosition, isValidSize, loadInspectorState, markTelemetryDisclosureShown, saveInspectorState };
|
|
197
|
+
export { clearLegacyAnnouncementReadState, getOrCreateTelemetryDistinctId, hasTelemetryDisclosureBeenShown, isTelemetryOptedOut, isValidAnchor, isValidDockMode, isValidPosition, isValidSize, loadAnnouncementPulsedTimestamp, loadAnnouncementReadTimestamp, loadInspectorState, markTelemetryDisclosureShown, saveAnnouncementPulsedTimestamp, saveAnnouncementReadTimestamp, saveInspectorState };
|
|
101
198
|
//# sourceMappingURL=persistence.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.mjs","names":[],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n};\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that banner_viewed and banner_clicked from\n// the same page-load share one distinct_id even without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"persistence.mjs","names":[],"sources":["../../src/lib/persistence.ts"],"sourcesContent":["import type { Anchor, DockMode, Position, Size } from \"./types.js\";\n\nexport type PersistedContextState = {\n anchor?: Anchor;\n anchorOffset?: Position;\n size?: Size;\n hasCustomPosition?: boolean;\n};\n\nexport type PersistedState = {\n button?: Omit<PersistedContextState, \"size\">;\n window?: PersistedContextState;\n isOpen?: boolean;\n dockMode?: DockMode;\n selectedMenu?: string;\n selectedContext?: string;\n hasOpenedInspector?: boolean;\n sidebarCollapsed?: boolean;\n /** @deprecated Replaced by colorSchemePreference to distinguish a user choice from the old light default. */\n colorScheme?: \"light\" | \"dark\";\n colorSchemePreference?: \"light\" | \"dark\";\n};\n\nexport const HOME_NEWS_READ_STORAGE_KEY = \"cpk:inspector:home-news-read\";\n\n/** Return story ids the user has already opened or hovered on Home. */\nexport function loadHomeNewsReadIds(storageKey: string): string[] {\n if (typeof window === \"undefined\") {\n return [];\n }\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n return [];\n }\n return parsed.filter((id): id is string => typeof id === \"string\");\n } catch {\n return [];\n }\n}\n\n/** Persist Home news story ids that the user has already seen. */\nexport function saveHomeNewsReadIds(storageKey: string, ids: string[]): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(ids));\n } catch (error) {\n console.warn(\"Failed to persist Home news read state\", error);\n }\n}\n\nexport function loadInspectorState(storageKey: string): PersistedState | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n const raw = window.localStorage.getItem(storageKey);\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n // Fall through to cookie migration path\n }\n }\n\n // Backwards compatibility: try to read the legacy cookie and migrate it\n if (typeof document !== \"undefined\") {\n const prefix = `${storageKey}=`;\n const entry = document.cookie\n .split(\"; \")\n .find((cookie) => cookie.startsWith(prefix));\n if (entry) {\n const legacyRaw = entry.substring(prefix.length);\n try {\n const parsed = JSON.parse(decodeURIComponent(legacyRaw));\n if (parsed && typeof parsed === \"object\") {\n return parsed as PersistedState;\n }\n } catch {\n return null;\n }\n }\n }\n\n return null;\n}\n\nexport function saveInspectorState(\n storageKey: string,\n state: PersistedState,\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to persist inspector state\", error);\n }\n}\n\nexport function isValidAnchor(value: unknown): value is Anchor {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Anchor;\n return (\n (candidate.horizontal === \"left\" || candidate.horizontal === \"right\") &&\n (candidate.vertical === \"top\" || candidate.vertical === \"bottom\")\n );\n}\n\nexport function isValidPosition(value: unknown): value is Position {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Position;\n return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y);\n}\n\nexport function isValidSize(value: unknown): value is Size {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n const candidate = value as Size;\n return isFiniteNumber(candidate.width) && isFiniteNumber(candidate.height);\n}\n\nexport function isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport function isValidDockMode(value: unknown): value is DockMode {\n return value === \"floating\" || value === \"docked-left\";\n}\n\n// Announcement read state — \"have I read this announcement\" is a property of\n// the person, not of the project, so it must survive a change of localhost\n// port. localStorage cannot express that: it is partitioned by origin and\n// origin includes the port, so :3000 and :5173 are separate stores. Cookies\n// are partitioned by host, so a cookie set on `localhost` without a `domain`\n// attribute is shared by every port on that host.\n//\n// Underscores, not colons: `:` is a separator in RFC 6265 and is not valid in\n// a cookie name. Browsers are lenient about it; we don't rely on that.\nconst ANNOUNCEMENT_READ_COOKIE_NAME = \"cpk_inspector_announcements\";\n\n// localStorage mirror of the cookie, so a browser that blocks cookies\n// degrades to per-port behaviour instead of losing the read state entirely.\n// A NEW key on purpose — the legacy one is abandoned, not migrated.\nconst ANNOUNCEMENT_READ_MIRROR_KEY = \"cpk:inspector:announcement_read\";\n\n// The superseded key. Every existing user is re-armed exactly once so they\n// discover the surface that replaced the announcement bubble, and the key is\n// deleted rather than left in place so nothing can fall back to it later.\nconst LEGACY_ANNOUNCEMENT_READ_KEY = \"cpk:inspector:announcements\";\n\n// Pulse suppression is per browser tab, and stores the announcement timestamp\n// rather than a boolean: a boolean would swallow a newly published\n// announcement for the rest of that tab's life, and the feed is fetched once\n// per mount with no polling.\nconst ANNOUNCEMENT_PULSED_SESSION_KEY = \"cpk:inspector:pulsed\";\n\n// Roughly one year. No `Secure` — local development is served over plain\n// HTTP — and no `HttpOnly`, because the component reads the value from\n// script. `Path=/` and `SameSite=Lax` keep it host-wide and same-site only.\nconst ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;\n\n/**\n * The announcement timestamp the user has already read, or `null` when\n * nothing has been read yet. Prefers the host-scoped cookie and falls back to\n * the origin-scoped mirror, so cookie-blocking browsers still remember the\n * read within the port they are on.\n */\nexport function loadAnnouncementReadTimestamp(): string | null {\n return (\n parseTimestampPayload(readAnnouncementCookie()) ??\n parseTimestampPayload(readLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY))\n );\n}\n\n/**\n * Records an announcement as read in both the host-scoped cookie and the\n * origin-scoped mirror. The stored value is `{\"timestamp\":\"…\"}` — the same\n * shape the announcement state has always used.\n */\nexport function saveAnnouncementReadTimestamp(timestamp: string): void {\n const payload = JSON.stringify({ timestamp });\n writeAnnouncementCookie(payload);\n writeLocalStorageItem(ANNOUNCEMENT_READ_MIRROR_KEY, payload);\n}\n\n/**\n * Deletes the superseded origin-scoped read state. Safe to call on every\n * startup: nothing writes that key any more, so the value cannot come back.\n */\nexport function clearLegacyAnnouncementReadState(): void {\n removeLocalStorageItem(LEGACY_ANNOUNCEMENT_READ_KEY);\n}\n\n/**\n * The announcement timestamp this browser tab has already pulsed for, or\n * `null` when it hasn't pulsed yet.\n */\nexport function loadAnnouncementPulsedTimestamp(): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(ANNOUNCEMENT_PULSED_SESSION_KEY);\n } catch {\n return null;\n }\n}\n\n/** Suppresses further pulses for this announcement in this browser tab. */\nexport function saveAnnouncementPulsedTimestamp(timestamp: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(ANNOUNCEMENT_PULSED_SESSION_KEY, timestamp);\n } catch {\n // No-op — a lost suppression costs one extra pulse, never correctness.\n }\n}\n\nfunction parseTimestampPayload(raw: string | null): string | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as { timestamp?: unknown };\n return typeof parsed?.timestamp === \"string\" ? parsed.timestamp : null;\n } catch {\n return null;\n }\n}\n\nfunction readAnnouncementCookie(): string | null {\n if (typeof document === \"undefined\") return null;\n try {\n for (const entry of document.cookie.split(\";\")) {\n const separator = entry.indexOf(\"=\");\n if (separator === -1) continue;\n if (entry.slice(0, separator).trim() !== ANNOUNCEMENT_READ_COOKIE_NAME) {\n continue;\n }\n return decodeURIComponent(entry.slice(separator + 1).trim());\n }\n } catch {\n // Cookie access throws in sandboxed documents — fall through to the\n // mirror rather than breaking the host app.\n }\n return null;\n}\n\nfunction writeAnnouncementCookie(value: string): void {\n if (typeof document === \"undefined\") return;\n try {\n document.cookie = `${ANNOUNCEMENT_READ_COOKIE_NAME}=${encodeURIComponent(\n value,\n )}; Path=/; Max-Age=${ANNOUNCEMENT_READ_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n } catch {\n // No-op — the mirror below is what a cookie-blocking browser falls back\n // to, and neither may break the host app.\n }\n}\n\nfunction readLocalStorageItem(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorageItem(key: string, value: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nfunction removeLocalStorageItem(key: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // No-op.\n }\n}\n\n// Telemetry persistence — flat per-key localStorage rather than the\n// JSON-blob shape used for window/dock state, because each value is\n// independent and we want to read/write them without round-tripping\n// the whole inspector state object.\nconst TELEMETRY_DISTINCT_ID_KEY = \"cpk:inspector:telemetry:distinct_id\";\nconst TELEMETRY_OPT_OUT_KEY = \"cpk:inspector:telemetry:opt_out\";\nconst TELEMETRY_DISCLOSURE_SHOWN_KEY =\n \"cpk:inspector:telemetry:disclosure_shown\";\n\n// Module-level fallback for when localStorage is unavailable (private mode,\n// quota exceeded, etc.). Cached so that whats_new_viewed and\n// whats_new_clicked from the same page-load share one distinct_id even\n// without persistent storage —\n// funnel coherence within a session is preserved even when storage fails.\nlet inMemoryFallbackId: string | null = null;\n\nexport function getOrCreateTelemetryDistinctId(): string {\n if (typeof window === \"undefined\") {\n // SSR / test fallback. A non-persistent ID is preferable to throwing\n // because telemetry must never break the host application.\n return generateUuidV4();\n }\n\n try {\n const existing = window.localStorage.getItem(TELEMETRY_DISTINCT_ID_KEY);\n if (existing && existing.length > 0) return existing;\n const fresh = generateUuidV4();\n window.localStorage.setItem(TELEMETRY_DISTINCT_ID_KEY, fresh);\n return fresh;\n } catch {\n return (inMemoryFallbackId ??= generateUuidV4());\n }\n}\n\n// Test-only reset so the in-memory fallback doesn't leak between test cases.\nexport function _resetTelemetryPersistenceForTesting(): void {\n inMemoryFallbackId = null;\n}\n\nexport function isTelemetryOptedOut(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === \"true\";\n } catch {\n return false;\n }\n}\n\nexport function setTelemetryOptOut(optedOut: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (optedOut) {\n window.localStorage.setItem(TELEMETRY_OPT_OUT_KEY, \"true\");\n } else {\n window.localStorage.removeItem(TELEMETRY_OPT_OUT_KEY);\n }\n } catch {\n // No-op — see getOrCreateTelemetryDistinctId.\n }\n}\n\nexport function hasTelemetryDisclosureBeenShown(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return (\n window.localStorage.getItem(TELEMETRY_DISCLOSURE_SHOWN_KEY) === \"true\"\n );\n } catch {\n return false;\n }\n}\n\nexport function markTelemetryDisclosureShown(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(TELEMETRY_DISCLOSURE_SHOWN_KEY, \"true\");\n } catch {\n // No-op.\n }\n}\n\nfunction generateUuidV4(): string {\n if (\n typeof globalThis.crypto !== \"undefined\" &&\n typeof globalThis.crypto.randomUUID === \"function\"\n ) {\n return globalThis.crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID (older browsers,\n // some test runners). Not cryptographically strong; acceptable because\n // the value is just an anonymous correlation ID.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n"],"mappings":"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"}
|
package/dist/lib/telemetry.cjs
CHANGED
|
@@ -4,9 +4,9 @@ const require_package = require('../package.cjs');
|
|
|
4
4
|
//#region src/lib/telemetry.ts
|
|
5
5
|
const TELEMETRY_EVENTS = {
|
|
6
6
|
opened: "oss.inspector.opened",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
whatsNewViewed: "oss.inspector.whats_new_viewed",
|
|
8
|
+
whatsNewSignalViewed: "oss.inspector.whats_new_signal_viewed",
|
|
9
|
+
whatsNewClicked: "oss.inspector.whats_new_clicked",
|
|
10
10
|
threadsTabClicked: "oss.inspector.threads_tab_clicked",
|
|
11
11
|
threadsLockedViewed: "oss.inspector.threads_locked_viewed",
|
|
12
12
|
threadsIntelligenceSignupClicked: "oss.inspector.threads_intelligence_signup_clicked",
|
|
@@ -22,6 +22,8 @@ const TELEMETRY_EVENTS = {
|
|
|
22
22
|
threadsExampleTourCompleted: "oss.inspector.threads_example_tour_completed",
|
|
23
23
|
threadsExampleTourReopened: "oss.inspector.threads_example_tour_reopened",
|
|
24
24
|
memoriesTabClicked: "oss.inspector.memories_tab_clicked",
|
|
25
|
+
homeViewed: "oss.inspector.home_viewed",
|
|
26
|
+
homeCtaClicked: "oss.inspector.home_cta_clicked",
|
|
25
27
|
metadataModuleViewed: "oss.inspector.metadata_module_viewed",
|
|
26
28
|
metadataActionClicked: "oss.inspector.metadata_action_clicked"
|
|
27
29
|
};
|
|
@@ -30,9 +32,6 @@ const TELEMETRY_DOCS_URL = "https://docs.copilotkit.ai/telemetry";
|
|
|
30
32
|
const PACKAGE_NAME = "@copilotkit/web-inspector";
|
|
31
33
|
const PACKAGE_VERSION = require_package.version;
|
|
32
34
|
const FETCH_TIMEOUT_MS = 3e3;
|
|
33
|
-
function isEnrichedTelemetryEvent(event) {
|
|
34
|
-
return event === TELEMETRY_EVENTS.opened || event === TELEMETRY_EVENTS.threadsTabClicked || event === TELEMETRY_EVENTS.threadsLockedViewed || event === TELEMETRY_EVENTS.threadsIntelligenceSignupClicked || event === TELEMETRY_EVENTS.threadsTalkToEngineerClicked || event === TELEMETRY_EVENTS.talkToEngineerClicked || event === TELEMETRY_EVENTS.threadsEmptyEnabledViewed || event === TELEMETRY_EVENTS.threadsEnabledViewed || event === TELEMETRY_EVENTS.threadsExampleViewed || event === TELEMETRY_EVENTS.threadsExampleSelected || event === TELEMETRY_EVENTS.threadsExampleTourStarted || event === TELEMETRY_EVENTS.threadsExampleTourStepViewed || event === TELEMETRY_EVENTS.threadsExampleTourDismissed || event === TELEMETRY_EVENTS.threadsExampleTourCompleted || event === TELEMETRY_EVENTS.threadsExampleTourReopened || event === TELEMETRY_EVENTS.memoriesTabClicked || event === TELEMETRY_EVENTS.metadataModuleViewed || event === TELEMETRY_EVENTS.metadataActionClicked;
|
|
35
|
-
}
|
|
36
35
|
function getRuntimeUrlType(runtimeUrl) {
|
|
37
36
|
if (!runtimeUrl) return "missing";
|
|
38
37
|
if (runtimeUrl.startsWith("/") && !runtimeUrl.startsWith("//")) return "relative";
|
|
@@ -59,47 +58,47 @@ function track(event, properties = {}) {
|
|
|
59
58
|
if (require_persistence.isTelemetryOptedOut()) return;
|
|
60
59
|
try {
|
|
61
60
|
const distinctId = require_persistence.getOrCreateTelemetryDistinctId();
|
|
62
|
-
const enrichedProperties = isEnrichedTelemetryEvent(event) ? {
|
|
63
|
-
package_name: PACKAGE_NAME,
|
|
64
|
-
package_version: PACKAGE_VERSION,
|
|
65
|
-
inspector_distinct_id: distinctId
|
|
66
|
-
} : {};
|
|
67
61
|
postBestEffort(TELEMETRY_INGEST_URL, JSON.stringify({
|
|
68
62
|
event,
|
|
69
63
|
properties: {
|
|
70
64
|
...properties,
|
|
71
|
-
|
|
65
|
+
package_name: PACKAGE_NAME,
|
|
66
|
+
package_version: PACKAGE_VERSION,
|
|
67
|
+
inspector_distinct_id: distinctId,
|
|
72
68
|
distinct_id: distinctId
|
|
73
69
|
},
|
|
74
70
|
package: {
|
|
75
71
|
name: PACKAGE_NAME,
|
|
76
|
-
|
|
72
|
+
version: PACKAGE_VERSION
|
|
77
73
|
},
|
|
78
74
|
ts: Math.floor(Date.now() / 1e3)
|
|
79
75
|
}), distinctId);
|
|
80
76
|
} catch {}
|
|
81
77
|
}
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Fires when What's new has rendered *with content*. A loading state is not
|
|
80
|
+
* an impression, so the metric cannot inflate itself by counting readers who
|
|
81
|
+
* arrived before the feed resolved.
|
|
82
|
+
*/
|
|
83
|
+
function trackWhatsNewViewed(props) {
|
|
84
|
+
track(TELEMETRY_EVENTS.whatsNewViewed, props);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
/** Fires when the unread launcher signal is presented in a visible tab. */
|
|
87
|
+
function trackWhatsNewSignalViewed(props) {
|
|
88
|
+
track(TELEMETRY_EVENTS.whatsNewSignalViewed, props);
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* zero them out. New reporting should prefer this event, whose `surface`
|
|
93
|
-
* says whether the user swatted the bubble away or dismissed the card
|
|
94
|
-
* after opening the panel.
|
|
91
|
+
* Fires for links followed inside the announcement body. `body` is the only
|
|
92
|
+
* call-to-action left: dismissal no longer exists, so there is nothing else a
|
|
93
|
+
* click on this surface can mean.
|
|
95
94
|
*/
|
|
96
|
-
function
|
|
97
|
-
track(TELEMETRY_EVENTS.
|
|
95
|
+
function trackWhatsNewClicked(props) {
|
|
96
|
+
track(TELEMETRY_EVENTS.whatsNewClicked, props);
|
|
98
97
|
}
|
|
99
98
|
/**
|
|
100
99
|
* Panel-open signal (OSS-566). Before this event, opens could only be
|
|
101
|
-
* inferred from in-panel activity (a floor) or from
|
|
102
|
-
*
|
|
100
|
+
* inferred from in-panel activity (a floor) or from the announcement's own
|
|
101
|
+
* click event (which misses the floating-button path entirely).
|
|
103
102
|
*/
|
|
104
103
|
function trackInspectorOpened(props) {
|
|
105
104
|
track(TELEMETRY_EVENTS.opened, props);
|
|
@@ -196,6 +195,19 @@ function trackThreadsExampleTourReopened(props) {
|
|
|
196
195
|
function trackMemoriesTabClicked(props = {}) {
|
|
197
196
|
track(TELEMETRY_EVENTS.memoriesTabClicked, props);
|
|
198
197
|
}
|
|
198
|
+
function trackHomeViewed(props = {}) {
|
|
199
|
+
track(TELEMETRY_EVENTS.homeViewed, {
|
|
200
|
+
group_key: props.group_key ?? "home",
|
|
201
|
+
leaf_key: props.leaf_key ?? "home"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function trackHomeCtaClicked(props) {
|
|
205
|
+
track(TELEMETRY_EVENTS.homeCtaClicked, {
|
|
206
|
+
action_kind: props.action_kind,
|
|
207
|
+
group_key: props.group_key ?? "home",
|
|
208
|
+
leaf_key: props.leaf_key ?? "home"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
199
211
|
function metadataCoarseProperties(props) {
|
|
200
212
|
const navigation = props.group_key === void 0 || props.leaf_key === void 0 ? {} : {
|
|
201
213
|
group_key: props.group_key,
|
|
@@ -235,13 +247,13 @@ function trackMetadataActionClicked(props) {
|
|
|
235
247
|
}
|
|
236
248
|
/**
|
|
237
249
|
* Returns the inspector's anonymous distinct-ID for cross-domain
|
|
238
|
-
* propagation onto outbound
|
|
250
|
+
* propagation onto outbound announcement-CTA links, or `null` when the user
|
|
239
251
|
* is opted out.
|
|
240
252
|
*
|
|
241
253
|
* The website / Ops API reads this query param on signup-flow landing
|
|
242
254
|
* pages and calls `posthog.alias(...)` to merge the inspector's anon
|
|
243
255
|
* ID with the website's anon ID, enabling the
|
|
244
|
-
* `
|
|
256
|
+
* `whats_new_viewed → whats_new_clicked → signup_attributed` funnel.
|
|
245
257
|
* `identify()` itself is out of scope here (it happens on signup, in
|
|
246
258
|
* the website / Ops API).
|
|
247
259
|
*
|
|
@@ -254,7 +266,7 @@ function getTelemetryDistinctIdForUrl() {
|
|
|
254
266
|
}
|
|
255
267
|
/**
|
|
256
268
|
* Seeds the anonymous distinct-ID into localStorage on inspector mount
|
|
257
|
-
* so it is ready for cross-domain propagation onto
|
|
269
|
+
* so it is ready for cross-domain propagation onto announcement-CTA links
|
|
258
270
|
* even before the first event fires. No-op when the user has opted out.
|
|
259
271
|
*/
|
|
260
272
|
function ensureTelemetryDistinctId() {
|
|
@@ -302,9 +314,8 @@ exports.ensureTelemetryDistinctId = ensureTelemetryDistinctId;
|
|
|
302
314
|
exports.getRuntimeUrlType = getRuntimeUrlType;
|
|
303
315
|
exports.getTelemetryDistinctIdForUrl = getTelemetryDistinctIdForUrl;
|
|
304
316
|
exports.maybeShowDisclosure = maybeShowDisclosure;
|
|
305
|
-
exports.
|
|
306
|
-
exports.
|
|
307
|
-
exports.trackBannerViewed = trackBannerViewed;
|
|
317
|
+
exports.trackHomeCtaClicked = trackHomeCtaClicked;
|
|
318
|
+
exports.trackHomeViewed = trackHomeViewed;
|
|
308
319
|
exports.trackInspectorOpened = trackInspectorOpened;
|
|
309
320
|
exports.trackMemoriesTabClicked = trackMemoriesTabClicked;
|
|
310
321
|
exports.trackMetadataActionClicked = trackMetadataActionClicked;
|
|
@@ -323,4 +334,7 @@ exports.trackThreadsIntelligenceSignupClicked = trackThreadsIntelligenceSignupCl
|
|
|
323
334
|
exports.trackThreadsLockedViewed = trackThreadsLockedViewed;
|
|
324
335
|
exports.trackThreadsTabClicked = trackThreadsTabClicked;
|
|
325
336
|
exports.trackThreadsTalkToEngineerClicked = trackThreadsTalkToEngineerClicked;
|
|
337
|
+
exports.trackWhatsNewClicked = trackWhatsNewClicked;
|
|
338
|
+
exports.trackWhatsNewSignalViewed = trackWhatsNewSignalViewed;
|
|
339
|
+
exports.trackWhatsNewViewed = trackWhatsNewViewed;
|
|
326
340
|
//# sourceMappingURL=telemetry.cjs.map
|