@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 +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +70 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +70 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-DX6B_xvr.d.mts → ranges-D0k0f-FF.d.mts} +19 -0
- package/dist/{ranges-DX6B_xvr.d.ts → ranges-D0k0f-FF.d.ts} +19 -0
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +37 -2
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +37 -2
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/reportData.ts +20 -0
- package/src/server/reports/measurementHealth.ts +65 -2
- package/src/server/reports/reports.test.ts +96 -0
- package/src/studio/Figure.tsx +100 -2
- package/src/studio/panels.test.tsx +82 -3
- package/src/studio/panels.tsx +24 -3
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
] })
|