@liiift-studio/sanity-visitor-insights 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/dist/index.d.mts +155 -0
  4. package/dist/index.d.ts +155 -0
  5. package/dist/index.js +652 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +609 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/ranges-D6AZwpmm.d.mts +282 -0
  10. package/dist/ranges-D6AZwpmm.d.ts +282 -0
  11. package/dist/server.d.mts +280 -0
  12. package/dist/server.d.ts +280 -0
  13. package/dist/server.js +937 -0
  14. package/dist/server.js.map +1 -0
  15. package/dist/server.mjs +882 -0
  16. package/dist/server.mjs.map +1 -0
  17. package/package.json +68 -0
  18. package/src/boundary.test.ts +93 -0
  19. package/src/core/core.test.ts +175 -0
  20. package/src/core/cutover.ts +109 -0
  21. package/src/core/ranges.ts +103 -0
  22. package/src/core/siteConfig.ts +150 -0
  23. package/src/index.ts +86 -0
  24. package/src/reportData.ts +120 -0
  25. package/src/server/auth.ts +133 -0
  26. package/src/server/cache.ts +75 -0
  27. package/src/server/createHandler.ts +199 -0
  28. package/src/server/ga4.ts +149 -0
  29. package/src/server/googleAuth.ts +139 -0
  30. package/src/server/orders.ts +127 -0
  31. package/src/server/reports/acquisition.ts +85 -0
  32. package/src/server/reports/journey.ts +112 -0
  33. package/src/server/reports/measurementHealth.ts +170 -0
  34. package/src/server/reports/typefaceInterest.ts +140 -0
  35. package/src/server/vercel.ts +68 -0
  36. package/src/server.ts +27 -0
  37. package/src/studio/Figure.tsx +175 -0
  38. package/src/studio/VisitorInsightsTool.tsx +247 -0
  39. package/src/studio/panels.tsx +233 -0
  40. package/src/studio/useReport.ts +99 -0
  41. package/src/types.ts +135 -0
package/dist/index.js ADDED
@@ -0,0 +1,652 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ComparisonBar: () => ComparisonBar,
24
+ MetricFigure: () => MetricFigure,
25
+ NoticeList: () => NoticeList,
26
+ PREEXISTING: () => PREEXISTING,
27
+ REPORT_NAMES: () => REPORT_NAMES,
28
+ VisitorInsightsTool: () => VisitorInsightsTool,
29
+ coverageForRange: () => coverageForRange,
30
+ default: () => index_default,
31
+ formatCount: () => formatCount,
32
+ formatPercent: () => formatPercent,
33
+ isReportName: () => isReportName,
34
+ ok: () => ok,
35
+ partial: () => partial,
36
+ previousRange: () => previousRange,
37
+ resolveRange: () => resolveRange,
38
+ unavailable: () => unavailable,
39
+ useReport: () => useReport,
40
+ validateSiteConfig: () => validateSiteConfig,
41
+ valueOrNull: () => valueOrNull,
42
+ visitorInsights: () => visitorInsights
43
+ });
44
+ module.exports = __toCommonJS(index_exports);
45
+ var import_sanity2 = require("sanity");
46
+ var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
47
+
48
+ // src/studio/VisitorInsightsTool.tsx
49
+ var import_react2 = require("react");
50
+ var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
51
+
52
+ // src/studio/useReport.ts
53
+ var import_react = require("react");
54
+ var import_sanity = require("sanity");
55
+ var API_VERSION = "2024-03-01";
56
+ function useReport({ apiBaseUrl, report, range }) {
57
+ const client = (0, import_sanity.useClient)({ apiVersion: API_VERSION });
58
+ const [state, setState] = (0, import_react.useState)({ status: "idle" });
59
+ const [nonce, setNonce] = (0, import_react.useState)(0);
60
+ const requestIdRef = (0, import_react.useRef)(0);
61
+ const reload = (0, import_react.useCallback)(() => setNonce((n) => n + 1), []);
62
+ (0, import_react.useEffect)(() => {
63
+ const requestId = requestIdRef.current + 1;
64
+ requestIdRef.current = requestId;
65
+ const controller = new AbortController();
66
+ setState({ status: "loading" });
67
+ async function run() {
68
+ const token = client.config().token;
69
+ if (!token) {
70
+ setState({
71
+ status: "error",
72
+ message: "No Sanity session token available in this Studio. Visitor Insights needs token-based auth to call the site API."
73
+ });
74
+ return;
75
+ }
76
+ try {
77
+ const url = `${apiBaseUrl.replace(/\/$/, "")}/api/visitor-insights/${report}?range=${range}`;
78
+ const response = await fetch(url, {
79
+ headers: { Authorization: `Bearer ${token}` },
80
+ signal: controller.signal
81
+ });
82
+ if (requestIdRef.current !== requestId) return;
83
+ if (!response.ok) {
84
+ const detail = response.status === 401 ? "Not authorised \u2014 sign in to the Studio again." : `Request failed (${response.status})`;
85
+ setState({ status: "error", message: detail });
86
+ return;
87
+ }
88
+ const envelope = await response.json();
89
+ if (requestIdRef.current !== requestId) return;
90
+ setState({ status: "ready", envelope });
91
+ } catch (e) {
92
+ if (controller.signal.aborted || requestIdRef.current !== requestId) return;
93
+ setState({ status: "error", message: e.message });
94
+ }
95
+ }
96
+ void run();
97
+ return () => controller.abort();
98
+ }, [client, apiBaseUrl, report, range, nonce]);
99
+ return { state, reload };
100
+ }
101
+
102
+ // src/studio/Figure.tsx
103
+ var import_sanity_ui_compat = require("@liiift-studio/sanity-ui-compat");
104
+ var import_jsx_runtime = require("react/jsx-runtime");
105
+ var REASON_TEXT = {
106
+ not_instrumented: "Not tracked on this site",
107
+ before_cutover: "Not tracked during this period",
108
+ suppressed: "Withheld by GA4 for privacy",
109
+ source_error: "Source did not respond",
110
+ not_applicable: "Does not apply to this site"
111
+ };
112
+ function formatCount(value) {
113
+ return new Intl.NumberFormat("en-GB").format(Math.round(value));
114
+ }
115
+ function formatPercent(ratio, digits = 0) {
116
+ return `${(ratio * 100).toFixed(digits)}%`;
117
+ }
118
+ function MetricFigure({ metric, label, size = 4 }) {
119
+ if (metric.status === "unavailable") {
120
+ const reason = REASON_TEXT[metric.reason];
121
+ const detail = metric.detail ? `${reason}. ${metric.detail}` : reason;
122
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Tooltip, { content: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Box, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: detail }) }), portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, muted: true, "aria-label": `${label}: unavailable. ${detail}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2014" }) }) });
123
+ }
124
+ const formatted = formatCount(metric.value);
125
+ if (metric.status === "partial") {
126
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, "aria-label": `${label}: ${formatted}, partial. ${metric.note}`, children: formatted }),
128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: "Partial" })
129
+ ] });
130
+ }
131
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, "aria-label": `${label}: ${formatted}`, children: formatted });
132
+ }
133
+ function ComparisonBar({ label, metric, max, tone = "default" }) {
134
+ const value = metric.status === "unavailable" ? null : metric.value;
135
+ const width = value !== null && max > 0 ? Math.max(2, value / max * 100) : 0;
136
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
137
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Flex, { align: "center", justify: "space-between", gap: 3, children: [
138
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: label }),
139
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricFigure, { metric, label, size: 1 })
140
+ ] }),
141
+ value === null ? (
142
+ // A dashed rail, not a zero-width bar: absence must not read as a measured zero.
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
144
+ import_sanity_ui_compat.Box,
145
+ {
146
+ "aria-hidden": "true",
147
+ style: {
148
+ height: 8,
149
+ borderRadius: 4,
150
+ border: "1px dashed var(--card-border-color)"
151
+ }
152
+ }
153
+ )
154
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
155
+ import_sanity_ui_compat.Box,
156
+ {
157
+ "aria-hidden": "true",
158
+ style: {
159
+ height: 8,
160
+ borderRadius: 4,
161
+ background: "var(--card-border-color)",
162
+ overflow: "hidden"
163
+ },
164
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
165
+ import_sanity_ui_compat.Box,
166
+ {
167
+ style: {
168
+ width: `${width}%`,
169
+ height: "100%",
170
+ background: tone === "primary" ? "var(--card-focus-ring-color)" : tone === "positive" ? "var(--card-badge-positive-dot-color, var(--card-focus-ring-color))" : tone === "caution" ? "var(--card-badge-caution-dot-color, var(--card-muted-fg-color))" : "var(--card-muted-fg-color)"
171
+ }
172
+ }
173
+ )
174
+ }
175
+ )
176
+ ] });
177
+ }
178
+ function NoticeList({ notices }) {
179
+ if (notices.length === 0) return null;
180
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 8 }, children: notices.map((notice) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Flex, { gap: 2, align: "flex-start", children: [
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, "aria-hidden": "true", children: "\u26A0" }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: notice })
183
+ ] }) }, notice)) });
184
+ }
185
+
186
+ // src/studio/panels.tsx
187
+ var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
188
+ var import_jsx_runtime2 = require("react/jsx-runtime");
189
+ function maxOf(metrics) {
190
+ return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
191
+ }
192
+ var tableWrap = { overflowX: "auto", width: "100%" };
193
+ var table = { width: "100%", borderCollapse: "collapse", minWidth: 420 };
194
+ var cell = { padding: "8px 12px", textAlign: "left", borderBottom: "1px solid var(--card-border-color)" };
195
+ var numericCell = { ...cell, textAlign: "right" };
196
+ function MeasurementHealthPanel({ data }) {
197
+ const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
198
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
199
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Pageviews, source against source" }),
201
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "The same unit on both sides. Vercel is cookieless and ungated; GA4 is consent-gated and blockable, so GA4 seeing fewer is expected." }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, tone: "primary" }),
203
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, tone: "default" })
204
+ ] }),
205
+ data.shortfallRatio !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: [
207
+ "GA4 shortfall: ",
208
+ formatPercent(Math.abs(data.shortfallRatio), 1)
209
+ ] }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretation })
211
+ ] }) }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
213
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Context" }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Different units to the figures above, and to each other. Shown for scale, never differenced." }),
215
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { gap: 4, wrap: "wrap", children: [
216
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
217
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "GA4 sessions" }),
218
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.ga4Sessions, label: "GA4 sessions" })
219
+ ] }),
220
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
221
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Orders" }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.orders, label: "Orders" })
223
+ ] }),
224
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
225
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Consent granted" }),
226
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, percent of sessions" })
227
+ ] })
228
+ ] })
229
+ ] })
230
+ ] });
231
+ }
232
+ function AcquisitionPanel({ data }) {
233
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
234
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { gap: 4, wrap: "wrap", children: [
235
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Sessions" }),
237
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatCount(data.totalSessions) })
238
+ ] }),
239
+ data.designIndustryShare !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
240
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "From design-industry referrers" }),
241
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatPercent(data.designIndustryShare, 1) })
242
+ ] }),
243
+ data.unattributedShare !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Unattributed" }),
245
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatPercent(data.unattributedShare, 1) })
246
+ ] })
247
+ ] }),
248
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Box, { style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
249
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("caption", { style: { textAlign: "left", paddingBottom: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Traffic sources by sessions" }) }),
250
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
251
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Source" }) }),
252
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Channel" }) }),
253
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Sessions" }) })
254
+ ] }) }),
255
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: data.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
256
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { gap: 2, align: "center", children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.source }),
258
+ row.designIndustry && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "\xB7 design industry" }),
259
+ row.unattributed && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "\xB7 unattributed" })
260
+ ] }) }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: row.channel }) }),
262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(row.sessions) }) })
263
+ ] }, `${row.source}-${row.channel}`)) })
264
+ ] }) }),
265
+ data.rowsWithheld && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NoticeList, { notices: ["GA4 withheld some low-traffic rows for privacy, so this list is shorter than reality."] })
266
+ ] });
267
+ }
268
+ function JourneyPanel({ data }) {
269
+ const max = maxOf(data.steps.map((step) => step.count));
270
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: data.approximationNote }) }),
272
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: data.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
273
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: step.label, metric: step.count, max, tone: "primary" }),
274
+ step.conversionFromPrevious !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
275
+ formatPercent(step.conversionFromPrevious, 1),
276
+ " of the previous measurable step"
277
+ ] })
278
+ ] }, step.key)) }),
279
+ data.topExitPages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
280
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Where sessions ended" }),
281
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Box, { style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
282
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
283
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Page" }) }),
284
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Exits" }) })
285
+ ] }) }),
286
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: data.topExitPages.map((page) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
287
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: page.path }) }),
288
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(page.exits) }) })
289
+ ] }, page.path)) })
290
+ ] }) })
291
+ ] })
292
+ ] });
293
+ }
294
+ function TypefaceInterestPanel({ data }) {
295
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
296
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
297
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Box, { style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
298
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("caption", { style: { textAlign: "left", paddingBottom: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Engagement by typeface" }) }),
299
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
300
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Typeface" }) }),
301
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Viewed" }) }),
302
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Tested" }) }),
303
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Bought" }) }),
304
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: "Test rate" }) })
305
+ ] }) }),
306
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: data.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
307
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.typeface }) }),
308
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.viewed, label: `${row.typeface} viewed`, size: 1 }) }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.tested, label: `${row.typeface} tested`, size: 1 }) }),
310
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.bought, label: `${row.typeface} bought`, size: 1 }) }),
311
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: row.testRate === null ? "\u2014" : formatPercent(row.testRate, 1) }) })
312
+ ] }, row.typeface)) })
313
+ ] }) })
314
+ ] });
315
+ }
316
+
317
+ // src/studio/VisitorInsightsTool.tsx
318
+ var import_jsx_runtime3 = require("react/jsx-runtime");
319
+ var RANGES = [
320
+ { key: "week", label: "Week" },
321
+ { key: "quarter", label: "Quarter" },
322
+ { key: "year", label: "Year" }
323
+ ];
324
+ var PANELS = [
325
+ { report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
326
+ { report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
327
+ { report: "journey", label: "Journey", blurb: "How far visitors get, and where they stop" },
328
+ { report: "typeface-interest", label: "Typeface interest", blurb: "Viewed, tested and bought, by family" }
329
+ ];
330
+ function RangeSelector({ value, onChange }) {
331
+ const refs = (0, import_react2.useRef)([]);
332
+ const onKeyDown = (0, import_react2.useCallback)(
333
+ (event) => {
334
+ const currentIndex = RANGES.findIndex((r) => r.key === value);
335
+ let nextIndex = null;
336
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % RANGES.length;
337
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + RANGES.length) % RANGES.length;
338
+ if (event.key === "Home") nextIndex = 0;
339
+ if (event.key === "End") nextIndex = RANGES.length - 1;
340
+ if (nextIndex === null) return;
341
+ event.preventDefault();
342
+ const next = RANGES[nextIndex];
343
+ if (!next) return;
344
+ onChange(next.key);
345
+ refs.current[nextIndex]?.focus();
346
+ },
347
+ [value, onChange]
348
+ );
349
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Flex, { role: "radiogroup", "aria-label": "Date range", gap: 1, onKeyDown, children: RANGES.map((range, index) => {
350
+ const selected = range.key === value;
351
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
352
+ import_sanity_ui_compat3.Button,
353
+ {
354
+ ref: (el) => {
355
+ refs.current[index] = el;
356
+ },
357
+ role: "radio",
358
+ "aria-checked": selected,
359
+ tabIndex: selected ? 0 : -1,
360
+ mode: selected ? "default" : "bleed",
361
+ tone: selected ? "primary" : "default",
362
+ text: range.label,
363
+ onClick: () => onChange(range.key),
364
+ fontSize: 1,
365
+ padding: 3
366
+ },
367
+ range.key
368
+ );
369
+ }) });
370
+ }
371
+ function PanelTabs({ value, onChange }) {
372
+ const refs = (0, import_react2.useRef)([]);
373
+ const onKeyDown = (0, import_react2.useCallback)(
374
+ (event) => {
375
+ const currentIndex = PANELS.findIndex((p) => p.report === value);
376
+ let nextIndex = null;
377
+ if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % PANELS.length;
378
+ if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + PANELS.length) % PANELS.length;
379
+ if (event.key === "Home") nextIndex = 0;
380
+ if (event.key === "End") nextIndex = PANELS.length - 1;
381
+ if (nextIndex === null) return;
382
+ event.preventDefault();
383
+ const next = PANELS[nextIndex];
384
+ if (!next) return;
385
+ onChange(next.report);
386
+ refs.current[nextIndex]?.focus();
387
+ },
388
+ [value, onChange]
389
+ );
390
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Flex, { role: "tablist", "aria-label": "Report", gap: 1, wrap: "wrap", onKeyDown, children: PANELS.map((panel, index) => {
391
+ const selected = panel.report === value;
392
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
393
+ import_sanity_ui_compat3.Button,
394
+ {
395
+ ref: (el) => {
396
+ refs.current[index] = el;
397
+ },
398
+ id: `tab-${panel.report}`,
399
+ role: "tab",
400
+ "aria-selected": selected,
401
+ "aria-controls": `panel-${panel.report}`,
402
+ tabIndex: selected ? 0 : -1,
403
+ mode: selected ? "default" : "bleed",
404
+ tone: selected ? "primary" : "default",
405
+ text: panel.label,
406
+ onClick: () => onChange(panel.report),
407
+ fontSize: 1,
408
+ padding: 3
409
+ },
410
+ panel.report
411
+ );
412
+ }) });
413
+ }
414
+ function ReportPanel({ report, apiBaseUrl, range }) {
415
+ const { state, reload } = useReport({ apiBaseUrl, report, range });
416
+ const liveMessage = state.status === "loading" ? "Loading report" : state.status === "ready" ? `${report} updated for the selected ${range}` : state.status === "error" ? `Report failed: ${state.message}` : "";
417
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
418
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Box, { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: liveMessage }),
419
+ state.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "center", gap: 3, padding: 4, children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Spinner, { muted: true }),
421
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Loading\u2026" })
422
+ ] }),
423
+ state.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 4, radius: 2, tone: "critical", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: state.message }),
425
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Button, { text: "Try again", mode: "ghost", fontSize: 1, onClick: reload }) })
426
+ ] }) }),
427
+ state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
428
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NoticeList, { notices: state.envelope.notices }),
429
+ report === "measurement-health" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MeasurementHealthPanel, { data: state.envelope.data }),
430
+ report === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AcquisitionPanel, { data: state.envelope.data }),
431
+ report === "journey" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(JourneyPanel, { data: state.envelope.data }),
432
+ report === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
434
+ state.envelope.range.start,
435
+ " to ",
436
+ state.envelope.range.end,
437
+ " (",
438
+ state.envelope.range.timezone,
439
+ ")"
440
+ ] })
441
+ ] })
442
+ ] });
443
+ }
444
+ function VisitorInsightsTool({ apiBaseUrl, siteLabel }) {
445
+ const [range, setRange] = (0, import_react2.useState)("week");
446
+ const [activePanel, setActivePanel] = (0, import_react2.useState)("measurement-health");
447
+ const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
448
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Container, { width: 4, padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 5, children: [
449
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
451
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 2, children: "Visitor insights" }),
452
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: siteLabel })
453
+ ] }),
454
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RangeSelector, { value: range, onChange: setRange })
455
+ ] }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PanelTabs, { value: activePanel, onChange: setActivePanel }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
458
+ import_sanity_ui_compat3.Box,
459
+ {
460
+ role: "tabpanel",
461
+ id: `panel-${activePanel}`,
462
+ "aria-labelledby": `tab-${activePanel}`,
463
+ tabIndex: 0,
464
+ style: { position: "relative" },
465
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
466
+ active && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: active.blurb }),
467
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReportPanel, { report: activePanel, apiBaseUrl, range })
468
+ ] })
469
+ }
470
+ )
471
+ ] }) });
472
+ }
473
+
474
+ // src/types.ts
475
+ function ok(value) {
476
+ return { status: "ok", value };
477
+ }
478
+ function unavailable(reason, detail) {
479
+ return { status: "unavailable", reason, detail };
480
+ }
481
+ function partial(value, coveredFrom, note) {
482
+ return { status: "partial", value, coveredFrom, note };
483
+ }
484
+ function valueOrNull(metric) {
485
+ return metric.status === "unavailable" ? null : metric.value;
486
+ }
487
+ var REPORT_NAMES = [
488
+ "measurement-health",
489
+ "acquisition",
490
+ "journey",
491
+ "typeface-interest"
492
+ ];
493
+ function isReportName(value) {
494
+ return typeof value === "string" && REPORT_NAMES.includes(value);
495
+ }
496
+
497
+ // src/core/cutover.ts
498
+ var PREEXISTING = "preexisting";
499
+ function coverageForRange(cutovers, eventName, range) {
500
+ if (!(eventName in cutovers)) return { status: "none", reason: "unknown_event" };
501
+ const cutover = cutovers[eventName];
502
+ if (cutover === null || cutover === void 0) return { status: "none", reason: "not_instrumented" };
503
+ if (cutover === PREEXISTING) return { status: "full" };
504
+ const cutoverTime = Date.parse(cutover);
505
+ if (Number.isNaN(cutoverTime)) return { status: "none", reason: "unknown_event" };
506
+ if (Date.parse(range.end) < cutoverTime) return { status: "none", reason: "before_cutover", cutover };
507
+ if (Date.parse(range.start) < cutoverTime) return { status: "partial", cutover };
508
+ return { status: "full" };
509
+ }
510
+
511
+ // src/core/siteConfig.ts
512
+ function validateSiteConfig(config) {
513
+ const problems = [];
514
+ if (!config) {
515
+ return [{ field: "config", message: "No site config supplied" }];
516
+ }
517
+ if (!config.siteId) problems.push({ field: "siteId", message: "Required" });
518
+ if (!config.label) problems.push({ field: "label", message: "Required" });
519
+ if (config.ga4) {
520
+ const { propertyId, timezone } = config.ga4;
521
+ if (!propertyId) {
522
+ problems.push({ field: "ga4.propertyId", message: "Required when ga4 is configured" });
523
+ } else if (propertyId.startsWith("G-")) {
524
+ problems.push({
525
+ field: "ga4.propertyId",
526
+ message: `Looks like a measurement id ("${propertyId}"). The Data API needs the numeric property id from GA4 Admin instead.`
527
+ });
528
+ } else if (!/^\d+$/.test(propertyId)) {
529
+ problems.push({ field: "ga4.propertyId", message: "Must be the numeric GA4 property id" });
530
+ }
531
+ if (!timezone) {
532
+ problems.push({ field: "ga4.timezone", message: "Required \u2014 ranges are anchored to it" });
533
+ } else if (!isValidTimeZone(timezone)) {
534
+ problems.push({ field: "ga4.timezone", message: `Not a recognised IANA timezone: ${timezone}` });
535
+ }
536
+ }
537
+ if (config.vercel && !config.vercel.projectId) {
538
+ problems.push({ field: "vercel.projectId", message: "Required when vercel is configured" });
539
+ }
540
+ if (!config.orders?.documentType) {
541
+ problems.push({ field: "orders.documentType", message: "Required" });
542
+ }
543
+ if (!config.eventCutovers) {
544
+ problems.push({ field: "eventCutovers", message: "Required \u2014 omit events the site does not have" });
545
+ }
546
+ for (const origin of config.allowedStudioOrigins ?? []) {
547
+ if (!/^https?:\/\//.test(origin)) {
548
+ problems.push({ field: "allowedStudioOrigins", message: `Must be a full origin including scheme: ${origin}` });
549
+ }
550
+ if (origin.endsWith("/")) {
551
+ problems.push({ field: "allowedStudioOrigins", message: `Must not have a trailing slash: ${origin}` });
552
+ }
553
+ }
554
+ return problems;
555
+ }
556
+ function isValidTimeZone(timeZone) {
557
+ try {
558
+ new Intl.DateTimeFormat("en-CA", { timeZone });
559
+ return true;
560
+ } catch {
561
+ return false;
562
+ }
563
+ }
564
+
565
+ // src/core/ranges.ts
566
+ function formatInTimeZone(date, timeZone) {
567
+ const parts = new Intl.DateTimeFormat("en-CA", {
568
+ timeZone,
569
+ year: "numeric",
570
+ month: "2-digit",
571
+ day: "2-digit"
572
+ }).formatToParts(date);
573
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
574
+ return `${get("year")}-${get("month")}-${get("day")}`;
575
+ }
576
+ function shiftDays(isoDate, days) {
577
+ const time = Date.parse(`${isoDate}T00:00:00Z`);
578
+ if (Number.isNaN(time)) throw new Error(`Invalid ISO date: ${isoDate}`);
579
+ return new Date(time + days * 864e5).toISOString().slice(0, 10);
580
+ }
581
+ function daysBetween(start, end) {
582
+ return Math.round((Date.parse(`${end}T00:00:00Z`) - Date.parse(`${start}T00:00:00Z`)) / 864e5) + 1;
583
+ }
584
+ var RANGE_DAYS = {
585
+ week: 7,
586
+ quarter: 91,
587
+ year: 365
588
+ };
589
+ function resolveRange(key, timezone, now = /* @__PURE__ */ new Date()) {
590
+ const end = formatInTimeZone(now, timezone);
591
+ const start = shiftDays(end, -(RANGE_DAYS[key] - 1));
592
+ return { key, start, end, timezone };
593
+ }
594
+ function previousRange(range) {
595
+ const span = daysBetween(range.start, range.end);
596
+ return {
597
+ key: range.key,
598
+ start: shiftDays(range.start, -span),
599
+ end: shiftDays(range.end, -span),
600
+ timezone: range.timezone
601
+ };
602
+ }
603
+
604
+ // src/index.ts
605
+ var ChartIcon = (0, import_icons.resolveIcon)("ChartUpwardIcon", "chart-upward");
606
+ var visitorInsights = (0, import_sanity2.definePlugin)((options) => {
607
+ const { apiBaseUrl, siteLabel, name = "visitor-insights", title = "Insights", roles } = options;
608
+ return {
609
+ name: "@liiift-studio/sanity-visitor-insights",
610
+ tools: (prev, { currentUser }) => {
611
+ if (roles && roles.length > 0) {
612
+ const userRoles = currentUser?.roles?.map((role) => role.name) ?? [];
613
+ const permitted = userRoles.some((role) => roles.includes(role));
614
+ if (!permitted) return prev;
615
+ }
616
+ return [
617
+ ...prev,
618
+ {
619
+ name,
620
+ title,
621
+ icon: ChartIcon,
622
+ component: VisitorInsightsTool,
623
+ options: { apiBaseUrl, siteLabel }
624
+ }
625
+ ];
626
+ }
627
+ };
628
+ });
629
+ var index_default = visitorInsights;
630
+ // Annotate the CommonJS export names for ESM import in node:
631
+ 0 && (module.exports = {
632
+ ComparisonBar,
633
+ MetricFigure,
634
+ NoticeList,
635
+ PREEXISTING,
636
+ REPORT_NAMES,
637
+ VisitorInsightsTool,
638
+ coverageForRange,
639
+ formatCount,
640
+ formatPercent,
641
+ isReportName,
642
+ ok,
643
+ partial,
644
+ previousRange,
645
+ resolveRange,
646
+ unavailable,
647
+ useReport,
648
+ validateSiteConfig,
649
+ valueOrNull,
650
+ visitorInsights
651
+ });
652
+ //# sourceMappingURL=index.js.map