@liiift-studio/sanity-visitor-insights 0.6.1 → 0.7.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/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DX6B_xvr.mjs';
4
- export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-DX6B_xvr.mjs';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-D0k0f-FF.mjs';
4
+ export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-D0k0f-FF.mjs';
5
5
 
6
6
  /**
7
7
  * The Visitor Insights Studio tool.
@@ -93,12 +93,24 @@ interface MetricFigureProps {
93
93
  /** Accessible label describing what this number counts. */
94
94
  label: string;
95
95
  size?: number;
96
+ /**
97
+ * What the number IS, which decides how it is written.
98
+ *
99
+ * MetricValue carries availability but not unit, so a percentage and a count arrive here
100
+ * indistinguishable and both used to be written with the count formatter. The consent rate is a
101
+ * 0-100 percentage: it rendered as a bare "84" beside "GA4 sessions 357" and "Orders 7", where
102
+ * the obvious reading is 84 sessions out of 357 — a quarter — when the truth is 84%. A missing
103
+ * suffix inverted the conclusion.
104
+ *
105
+ * 'percent' expects a 0-100 value, matching what measurementHealth produces.
106
+ */
107
+ unit?: 'count' | 'percent';
96
108
  }
97
109
  /**
98
110
  * Render a metric value, handling the absent case visibly.
99
111
  * Screen readers get the reason text rather than an unexplained dash.
100
112
  */
101
- declare function MetricFigure({ metric, label, size }: MetricFigureProps): React.ReactElement;
113
+ declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
102
114
  /** Props for ComparisonBar. */
103
115
  interface ComparisonBarProps {
104
116
  label: string;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DX6B_xvr.js';
4
- export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-DX6B_xvr.js';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-D0k0f-FF.js';
4
+ export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-D0k0f-FF.js';
5
5
 
6
6
  /**
7
7
  * The Visitor Insights Studio tool.
@@ -93,12 +93,24 @@ interface MetricFigureProps {
93
93
  /** Accessible label describing what this number counts. */
94
94
  label: string;
95
95
  size?: number;
96
+ /**
97
+ * What the number IS, which decides how it is written.
98
+ *
99
+ * MetricValue carries availability but not unit, so a percentage and a count arrive here
100
+ * indistinguishable and both used to be written with the count formatter. The consent rate is a
101
+ * 0-100 percentage: it rendered as a bare "84" beside "GA4 sessions 357" and "Orders 7", where
102
+ * the obvious reading is 84 sessions out of 357 — a quarter — when the truth is 84%. A missing
103
+ * suffix inverted the conclusion.
104
+ *
105
+ * 'percent' expects a 0-100 value, matching what measurementHealth produces.
106
+ */
107
+ unit?: 'count' | 'percent';
96
108
  }
97
109
  /**
98
110
  * Render a metric value, handling the absent case visibly.
99
111
  * Screen readers get the reason text rather than an unexplained dash.
100
112
  */
101
- declare function MetricFigure({ metric, label, size }: MetricFigureProps): React.ReactElement;
113
+ declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
102
114
  /** Props for ComparisonBar. */
103
115
  interface ComparisonBarProps {
104
116
  label: string;
package/dist/index.js CHANGED
@@ -122,13 +122,13 @@ function formatCount(value) {
122
122
  function formatPercent(ratio, digits = 0) {
123
123
  return `${(ratio * 100).toFixed(digits)}%`;
124
124
  }
125
- function MetricFigure({ metric, label, size = 4 }) {
125
+ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
126
126
  if (metric.status === "unavailable") {
127
127
  const reason = REASON_TEXT[metric.reason];
128
128
  const detail = metric.detail ? `${reason}. ${metric.detail}` : reason;
129
129
  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" }) }) });
130
130
  }
131
- const formatted = formatCount(metric.value);
131
+ const formatted = unit === "percent" ? `${metric.value.toFixed(1)}%` : formatCount(metric.value);
132
132
  if (metric.status === "partial") {
133
133
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
134
134
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, "aria-label": `${label}: ${formatted}, partial. ${metric.note}`, children: formatted }),
@@ -171,6 +171,67 @@ function NoticeList({ notices }) {
171
171
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: notice })
172
172
  ] }) }) }, notice)) });
173
173
  }
174
+ function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
175
+ if (points.length < 3) return null;
176
+ const W = 720;
177
+ const H = 132;
178
+ const PAD = 4;
179
+ const values = points.flatMap((p) => [p.ga4, p.vercel]).filter((v) => typeof v === "number");
180
+ if (values.length === 0) return null;
181
+ const max = Math.max(...values, 1);
182
+ const x = (i) => PAD + i / Math.max(1, points.length - 1) * (W - PAD * 2);
183
+ const y = (v) => H - PAD - v / max * (H - PAD * 2);
184
+ const path = (pick) => {
185
+ let d = "";
186
+ let penDown = false;
187
+ points.forEach((p, i) => {
188
+ const v = pick(p);
189
+ if (typeof v !== "number") {
190
+ penDown = false;
191
+ return;
192
+ }
193
+ d += `${penDown ? "L" : "M"}${x(i).toFixed(1)} ${y(v).toFixed(1)} `;
194
+ penDown = true;
195
+ });
196
+ return d.trim();
197
+ };
198
+ const vercelPath = path((p) => p.vercel);
199
+ const ga4Path = path((p) => p.ga4);
200
+ const first = points[0]?.date ?? "";
201
+ const last = points[points.length - 1]?.date ?? "";
202
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
204
+ "svg",
205
+ {
206
+ viewBox: `0 0 ${W} ${H}`,
207
+ width: "100%",
208
+ height: H,
209
+ role: "img",
210
+ "aria-label": `${label}, ${first} to ${last}. Peak ${Math.round(max)} pageviews in a day.`,
211
+ preserveAspectRatio: "none",
212
+ style: { display: "block", overflow: "visible" },
213
+ children: [
214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: vercelPath, fill: "none", stroke: "currentColor", strokeOpacity: "0.85", strokeWidth: "2", vectorEffect: "non-scaling-stroke" }),
215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: ga4Path, fill: "none", stroke: "currentColor", strokeOpacity: "0.85", strokeWidth: "2", strokeDasharray: "5 3", vectorEffect: "non-scaling-stroke" })
216
+ ]
217
+ }
218
+ ) }),
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Flex, { gap: 3, align: "center", wrap: "wrap", children: [
220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2501\u2501 Vercel" }),
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2504\u2504 GA4" }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
223
+ first,
224
+ " to ",
225
+ last
226
+ ] }),
227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
228
+ "peak ",
229
+ formatCount(max),
230
+ "/day"
231
+ ] })
232
+ ] })
233
+ ] });
234
+ }
174
235
 
175
236
  // src/studio/VisitorInsightsTool.tsx
176
237
  var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
@@ -195,12 +256,14 @@ function MeasurementHealthPanel({ data }) {
195
256
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, tone: "default" })
196
257
  ] }),
197
258
  /* @__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: [
198
- data.shortfallRatio !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: [
199
- "GA4 shortfall: ",
200
- formatPercent(Math.abs(data.shortfallRatio), 1)
201
- ] }),
259
+ data.shortfallRatio !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: data.shortfallRatio >= 0 ? `GA4 saw ${formatPercent(data.shortfallRatio, 1)} fewer pageviews than Vercel` : `GA4 saw ${formatPercent(-data.shortfallRatio, 1)} more pageviews than Vercel` }),
202
260
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretation })
203
261
  ] }) }),
262
+ data.daily.length >= 3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
263
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Day by day" }),
264
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "A gap that has always been there is consent and blocking. A gap that opens on one day is an incident." }),
265
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrendChart, { points: data.daily })
266
+ ] }),
204
267
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
205
268
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Context" }),
206
269
  /* @__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,7 +278,7 @@ function MeasurementHealthPanel({ data }) {
215
278
  ] }) }),
216
279
  /* @__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: 3, children: [
217
280
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Consent granted" }),
218
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, percent of sessions" })
281
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, percent of sessions", unit: "percent" })
219
282
  ] }) })
220
283
  ] })
221
284
  ] })