@liiift-studio/sanity-visitor-insights 0.36.0 → 0.38.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 +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +41 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -16
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +21 -18
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +21 -18
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-TiuokGk8.d.mts → vercel-BG4eJo0f.d.mts} +10 -4
- package/dist/{vercel-TiuokGk8.d.ts → vercel-BG4eJo0f.d.ts} +10 -4
- package/package.json +1 -1
- package/src/server/createHandler.ts +41 -2
- package/src/server/diagnostics.ts +1 -1
- package/src/server/ga4.ts +24 -19
- package/src/server/reports/measurementHealth.ts +1 -1
- package/src/server/reports/reports.test.ts +43 -16
- package/src/server/vercel.ts +46 -28
- package/src/studio/CrossSourceTimeline.tsx +86 -19
- package/src/studio/Figure.tsx +36 -13
- package/src/studio/panels.test.tsx +111 -1
- package/src/studio/panels.tsx +10 -0
package/dist/index.d.mts
CHANGED
|
@@ -467,6 +467,20 @@ interface Series {
|
|
|
467
467
|
* a site running at a flat 20% would show a full-height line and read as healthy.
|
|
468
468
|
*/
|
|
469
469
|
domain?: [number, number];
|
|
470
|
+
/**
|
|
471
|
+
* The same series in the previous equivalent window, aligned by day OFFSET rather than by date.
|
|
472
|
+
*
|
|
473
|
+
* Drawn as a quiet ghost behind the row. Without it every row is an unanchored silhouette: a
|
|
474
|
+
* flat line at 60 and a flat line at 6,000 are the same picture, and "traffic looks lumpy" is an
|
|
475
|
+
* impression nobody can falsify. The comparison envelope is already fetched and already handed
|
|
476
|
+
* to the metric cards, so this costs a prop and no network.
|
|
477
|
+
*
|
|
478
|
+
* What it buys is bigger than a reference line. It turns the tool's most fragile asset — a dated
|
|
479
|
+
* alarm produced by a heuristic with a 21-day floor and a percentile threshold — from something
|
|
480
|
+
* the reader has to trust into something they can check: the coverage row's step reads against
|
|
481
|
+
* last period's flat line whether or not the detector fired.
|
|
482
|
+
*/
|
|
483
|
+
comparison?: SeriesPoint[];
|
|
470
484
|
points: SeriesPoint[];
|
|
471
485
|
/**
|
|
472
486
|
* What a lossier source saw of the same thing.
|
package/dist/index.d.ts
CHANGED
|
@@ -467,6 +467,20 @@ interface Series {
|
|
|
467
467
|
* a site running at a flat 20% would show a full-height line and read as healthy.
|
|
468
468
|
*/
|
|
469
469
|
domain?: [number, number];
|
|
470
|
+
/**
|
|
471
|
+
* The same series in the previous equivalent window, aligned by day OFFSET rather than by date.
|
|
472
|
+
*
|
|
473
|
+
* Drawn as a quiet ghost behind the row. Without it every row is an unanchored silhouette: a
|
|
474
|
+
* flat line at 60 and a flat line at 6,000 are the same picture, and "traffic looks lumpy" is an
|
|
475
|
+
* impression nobody can falsify. The comparison envelope is already fetched and already handed
|
|
476
|
+
* to the metric cards, so this costs a prop and no network.
|
|
477
|
+
*
|
|
478
|
+
* What it buys is bigger than a reference line. It turns the tool's most fragile asset — a dated
|
|
479
|
+
* alarm produced by a heuristic with a 21-day floor and a percentile threshold — from something
|
|
480
|
+
* the reader has to trust into something they can check: the coverage row's step reads against
|
|
481
|
+
* last period's flat line whether or not the detector fired.
|
|
482
|
+
*/
|
|
483
|
+
comparison?: SeriesPoint[];
|
|
470
484
|
points: SeriesPoint[];
|
|
471
485
|
/**
|
|
472
486
|
* What a lossier source saw of the same thing.
|
package/dist/index.js
CHANGED
|
@@ -491,6 +491,7 @@ function ProportionChart({ bars, format, totalLabel }) {
|
|
|
491
491
|
] })
|
|
492
492
|
] });
|
|
493
493
|
}
|
|
494
|
+
var MIN_RATE_DENOMINATOR = 30;
|
|
494
495
|
function FunnelChart({ stages, measurement }) {
|
|
495
496
|
const [activeKey, setActiveKey] = (0, import_react2.useState)(null);
|
|
496
497
|
const refs = (0, import_react2.useRef)([]);
|
|
@@ -535,8 +536,8 @@ function FunnelChart({ stages, measurement }) {
|
|
|
535
536
|
] }),
|
|
536
537
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${width}%` } }) }),
|
|
537
538
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
538
|
-
index === 0 ? "entry step" : `${formatPercent(share, 1)} of ${stages[0]?.label.toLowerCase()}
|
|
539
|
-
index > 1 && stage.conversionFromPrevious !== null && previous ? ` \xB7 ${formatPercent(stage.conversionFromPrevious, 1)} of ${previous.label.toLowerCase()}` : ""
|
|
539
|
+
index === 0 ? "entry step" : entry >= MIN_RATE_DENOMINATOR && stage.value >= MIN_RATE_DENOMINATOR ? `${formatPercent(share, 1)} of ${stages[0]?.label.toLowerCase()}` : "too few to give a rate",
|
|
540
|
+
index > 1 && stage.conversionFromPrevious !== null && previous ? (previous.value ?? 0) >= MIN_RATE_DENOMINATOR && stage.value >= MIN_RATE_DENOMINATOR ? ` \xB7 ${formatPercent(stage.conversionFromPrevious, 1)} of ${previous.label.toLowerCase()}` : ` \xB7 too few from ${previous.label.toLowerCase()} to give a rate` : ""
|
|
540
541
|
] })
|
|
541
542
|
]
|
|
542
543
|
}
|
|
@@ -919,7 +920,7 @@ var import_d3_shape = require("d3-shape");
|
|
|
919
920
|
var import_d3_array = require("d3-array");
|
|
920
921
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
921
922
|
var WIDTH = 760;
|
|
922
|
-
var
|
|
923
|
+
var BASELINE_GUTTER = 18;
|
|
923
924
|
var AXIS_HEIGHT = 26;
|
|
924
925
|
var GUTTER = 52;
|
|
925
926
|
var RIGHT_PAD = 12;
|
|
@@ -1012,6 +1013,8 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1012
1013
|
return [...all].sort();
|
|
1013
1014
|
}, [series]);
|
|
1014
1015
|
const plotWidth = Math.max(120, measured - GUTTER - RIGHT_PAD);
|
|
1016
|
+
const bandHeight = Math.round(Math.max(58, Math.min(150, plotWidth / 7)));
|
|
1017
|
+
const rowHeight = bandHeight + LABEL_STRIP + BASELINE_GUTTER;
|
|
1015
1018
|
import_react3.default.useLayoutEffect(() => {
|
|
1016
1019
|
const frame = frameNode;
|
|
1017
1020
|
if (!frame) return;
|
|
@@ -1029,7 +1032,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1029
1032
|
observer.observe(frame);
|
|
1030
1033
|
return () => observer.disconnect();
|
|
1031
1034
|
}, [frameNode]);
|
|
1032
|
-
const height = TOP_PAD + series.length *
|
|
1035
|
+
const height = TOP_PAD + series.length * rowHeight + AXIS_HEIGHT;
|
|
1033
1036
|
const x = (0, import_react3.useMemo)(() => {
|
|
1034
1037
|
if (dates.length === 0) return null;
|
|
1035
1038
|
return (0, import_d3_scale.scaleUtc)().domain([/* @__PURE__ */ new Date(`${dates[0]}T00:00:00Z`), /* @__PURE__ */ new Date(`${dates[dates.length - 1]}T00:00:00Z`)]).range([GUTTER, GUTTER + plotWidth]);
|
|
@@ -1216,17 +1219,17 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1216
1219
|
x1: at,
|
|
1217
1220
|
x2: at,
|
|
1218
1221
|
y1: TOP_PAD,
|
|
1219
|
-
y2:
|
|
1222
|
+
y2: height - AXIS_HEIGHT + 4,
|
|
1220
1223
|
stroke: "currentColor",
|
|
1221
1224
|
strokeWidth: 1,
|
|
1222
|
-
opacity: 0.
|
|
1225
|
+
opacity: 0.18
|
|
1223
1226
|
},
|
|
1224
1227
|
`grid-${date}`
|
|
1225
1228
|
);
|
|
1226
1229
|
}),
|
|
1227
1230
|
series.map((row, rowIndex) => {
|
|
1228
|
-
const top = TOP_PAD + rowIndex *
|
|
1229
|
-
const bottom = top +
|
|
1231
|
+
const top = TOP_PAD + rowIndex * rowHeight;
|
|
1232
|
+
const bottom = top + rowHeight - BASELINE_GUTTER;
|
|
1230
1233
|
const plotTop = top + LABEL_STRIP;
|
|
1231
1234
|
const values = row.points.map((p) => p.value).filter((v) => v !== null);
|
|
1232
1235
|
const peak = (0, import_d3_array.max)(values) ?? 0;
|
|
@@ -1235,11 +1238,15 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1235
1238
|
const zeroY = y(0);
|
|
1236
1239
|
const at = (p) => x(/* @__PURE__ */ new Date(`${p.date}T00:00:00Z`));
|
|
1237
1240
|
const defined = (p) => p.value !== null;
|
|
1238
|
-
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.
|
|
1241
|
+
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveLinear);
|
|
1239
1242
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
1240
1243
|
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
1241
|
-
const gapGen = (0, import_d3_shape.area)().defined((p) => p.value !== null && byDate.get(p.date) != null).x(at).y0((p) => y(byDate.get(p.date))).y1((p) => y(p.value)).curve(import_d3_shape.
|
|
1244
|
+
const gapGen = (0, import_d3_shape.area)().defined((p) => p.value !== null && byDate.get(p.date) != null).x(at).y0((p) => y(byDate.get(p.date))).y1((p) => y(p.value)).curve(import_d3_shape.curveLinear);
|
|
1242
1245
|
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
1246
|
+
const ghost = row.comparison && row.comparison.length > 1 ? lineGen(row.comparison.slice(0, dates.length).map((point, i) => ({
|
|
1247
|
+
date: dates[i] ?? point.date,
|
|
1248
|
+
value: point.value
|
|
1249
|
+
}))) ?? "" : "";
|
|
1243
1250
|
const pitch = plotWidth / Math.max(1, dates.length - 1);
|
|
1244
1251
|
const stemWidth = Math.max(1, Math.min(9, pitch > 2 ? pitch - 1 : pitch));
|
|
1245
1252
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
@@ -1258,6 +1265,17 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1258
1265
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: y(y.domain()[1]) + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(y.domain()[1], row.unit, currency) }),
|
|
1259
1266
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: bottom + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(0, row.unit, currency) }),
|
|
1260
1267
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { clipPath: `url(#${clipId})`, children: [
|
|
1268
|
+
ghost && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1269
|
+
"path",
|
|
1270
|
+
{
|
|
1271
|
+
d: ghost,
|
|
1272
|
+
fill: "none",
|
|
1273
|
+
stroke: "currentColor",
|
|
1274
|
+
strokeWidth: 1,
|
|
1275
|
+
strokeDasharray: "2 3",
|
|
1276
|
+
opacity: 0.35
|
|
1277
|
+
}
|
|
1278
|
+
),
|
|
1261
1279
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1262
1280
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
1263
1281
|
zeroes.map((p) => {
|
|
@@ -1317,7 +1335,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1317
1335
|
x1: at,
|
|
1318
1336
|
x2: at,
|
|
1319
1337
|
y1: TOP_PAD - 6,
|
|
1320
|
-
y2: TOP_PAD + series.length *
|
|
1338
|
+
y2: TOP_PAD + series.length * rowHeight - BASELINE_GUTTER,
|
|
1321
1339
|
stroke: "currentColor",
|
|
1322
1340
|
strokeWidth: 1.25,
|
|
1323
1341
|
strokeDasharray: "4 3",
|
|
@@ -1334,7 +1352,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1334
1352
|
x: GUTTER,
|
|
1335
1353
|
y: TOP_PAD - 8,
|
|
1336
1354
|
width: Math.max(0, x(/* @__PURE__ */ new Date(`${dates[brushed[0]]}T00:00:00Z`)) - GUTTER),
|
|
1337
|
-
height: series.length *
|
|
1355
|
+
height: series.length * rowHeight - 10,
|
|
1338
1356
|
fill: "var(--card-bg-color, #ffffff)",
|
|
1339
1357
|
opacity: 0.72
|
|
1340
1358
|
}
|
|
@@ -1345,7 +1363,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1345
1363
|
x: x(/* @__PURE__ */ new Date(`${dates[brushed[1]]}T00:00:00Z`)),
|
|
1346
1364
|
y: TOP_PAD - 8,
|
|
1347
1365
|
width: Math.max(0, GUTTER + plotWidth - x(/* @__PURE__ */ new Date(`${dates[brushed[1]]}T00:00:00Z`))),
|
|
1348
|
-
height: series.length *
|
|
1366
|
+
height: series.length * rowHeight - 10,
|
|
1349
1367
|
fill: "var(--card-bg-color, #ffffff)",
|
|
1350
1368
|
opacity: 0.72
|
|
1351
1369
|
}
|
|
@@ -1357,7 +1375,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1357
1375
|
x1: x(/* @__PURE__ */ new Date(`${hoveredDate}T00:00:00Z`)),
|
|
1358
1376
|
x2: x(/* @__PURE__ */ new Date(`${hoveredDate}T00:00:00Z`)),
|
|
1359
1377
|
y1: TOP_PAD - 6,
|
|
1360
|
-
y2: TOP_PAD + series.length *
|
|
1378
|
+
y2: TOP_PAD + series.length * rowHeight - 18,
|
|
1361
1379
|
stroke: "currentColor",
|
|
1362
1380
|
strokeWidth: 1,
|
|
1363
1381
|
opacity: 0.8
|
|
@@ -1443,7 +1461,7 @@ var readoutRow = {
|
|
|
1443
1461
|
gap: 14,
|
|
1444
1462
|
flexWrap: "wrap",
|
|
1445
1463
|
alignItems: "baseline",
|
|
1446
|
-
minHeight:
|
|
1464
|
+
minHeight: 54
|
|
1447
1465
|
};
|
|
1448
1466
|
var revealButton = {
|
|
1449
1467
|
appearance: "none",
|
|
@@ -1577,6 +1595,8 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1577
1595
|
complete: true,
|
|
1578
1596
|
unit: "count",
|
|
1579
1597
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.vercelPageviews })),
|
|
1598
|
+
// The same row last period, so the reader can see whether this shape is normal.
|
|
1599
|
+
comparison: (previous?.crossSource ?? []).map((d) => ({ date: d.date, value: d.vercelPageviews })),
|
|
1580
1600
|
shortfall: {
|
|
1581
1601
|
label: "Seen by GA4",
|
|
1582
1602
|
source: "GA4",
|
|
@@ -1616,6 +1636,12 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1616
1636
|
points: (data.crossSource ?? []).map((d) => ({
|
|
1617
1637
|
date: d.date,
|
|
1618
1638
|
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? Math.min(1, d.ga4Pageviews / d.vercelPageviews) : null
|
|
1639
|
+
})),
|
|
1640
|
+
// The most valuable ghost in the chart: a collapse reads as a step away
|
|
1641
|
+
// from last period's flat line, whether or not the detector fired.
|
|
1642
|
+
comparison: (previous?.crossSource ?? []).map((d) => ({
|
|
1643
|
+
date: d.date,
|
|
1644
|
+
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? Math.min(1, d.ga4Pageviews / d.vercelPageviews) : null
|
|
1619
1645
|
}))
|
|
1620
1646
|
}] : [],
|
|
1621
1647
|
...data.crossSource?.some((d) => d.revenue !== null) ? [{
|