@liiift-studio/sanity-visitor-insights 0.18.0 → 0.19.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 +2 -10
- package/dist/index.d.ts +2 -10
- package/dist/index.js +194 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +195 -61
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-B_OrmcdP.d.mts → ranges-BUZuS1c-.d.mts} +10 -1
- package/dist/{ranges-B_OrmcdP.d.ts → ranges-BUZuS1c-.d.ts} +10 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +3 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/reportData.ts +10 -1
- package/src/server/reports/measurementHealth.ts +3 -0
- package/src/studio/CrossSourceTimeline.tsx +128 -52
- package/src/studio/Figure.tsx +67 -20
- package/src/studio/VisitorInsightsTool.tsx +59 -2
- package/src/studio/panels.test.tsx +65 -6
- package/src/studio/panels.tsx +10 -5
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 CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K 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-BUZuS1c-.mjs';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-BUZuS1c-.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
|
@@ -373,14 +373,6 @@ interface Series {
|
|
|
373
373
|
source: Series['source'];
|
|
374
374
|
points: SeriesPoint[];
|
|
375
375
|
};
|
|
376
|
-
/**
|
|
377
|
-
* Multiplier from observed to estimated-true, where the line itself is the lossy source.
|
|
378
|
-
*
|
|
379
|
-
* This one IS a symmetric band, because it is genuine uncertainty rather than a known blind
|
|
380
|
-
* spot. The two must not look alike: one says "we do not know exactly", the other says "we know
|
|
381
|
-
* exactly, and this much was invisible".
|
|
382
|
-
*/
|
|
383
|
-
grossUpFactor?: number;
|
|
384
376
|
}
|
|
385
377
|
/** A dated event drawn through every row, e.g. a campaign send. */
|
|
386
378
|
interface TimelineMarker {
|
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 CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K 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-BUZuS1c-.js';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-BUZuS1c-.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
|
@@ -373,14 +373,6 @@ interface Series {
|
|
|
373
373
|
source: Series['source'];
|
|
374
374
|
points: SeriesPoint[];
|
|
375
375
|
};
|
|
376
|
-
/**
|
|
377
|
-
* Multiplier from observed to estimated-true, where the line itself is the lossy source.
|
|
378
|
-
*
|
|
379
|
-
* This one IS a symmetric band, because it is genuine uncertainty rather than a known blind
|
|
380
|
-
* spot. The two must not look alike: one says "we do not know exactly", the other says "we know
|
|
381
|
-
* exactly, and this much was invisible".
|
|
382
|
-
*/
|
|
383
|
-
grossUpFactor?: number;
|
|
384
376
|
}
|
|
385
377
|
/** A dated event drawn through every row, e.g. a campaign send. */
|
|
386
378
|
interface TimelineMarker {
|
package/dist/index.js
CHANGED
|
@@ -69,7 +69,7 @@ var import_sanity2 = require("sanity");
|
|
|
69
69
|
var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
|
|
70
70
|
|
|
71
71
|
// src/studio/VisitorInsightsTool.tsx
|
|
72
|
-
var import_react4 = require("react");
|
|
72
|
+
var import_react4 = __toESM(require("react"));
|
|
73
73
|
var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
|
|
74
74
|
|
|
75
75
|
// src/studio/useReport.ts
|
|
@@ -205,23 +205,56 @@ function deltaStyle(tone) {
|
|
|
205
205
|
}
|
|
206
206
|
function MetricFigure({ metric, label, size = 4, unit = "count" }) {
|
|
207
207
|
if (metric.status === "unavailable") {
|
|
208
|
-
const reason = REASON_TEXT[metric.reason];
|
|
208
|
+
const reason = REASON_TEXT[metric.reason] ?? "No figure available";
|
|
209
209
|
const detail = metric.detail ? `${reason}. ${metric.detail}` : reason;
|
|
210
|
-
return
|
|
210
|
+
return (
|
|
211
|
+
// The reason travels as REAL TEXT in a visually-hidden span, not as an aria-label and not
|
|
212
|
+
// only in a tooltip.
|
|
213
|
+
//
|
|
214
|
+
// `aria-label` on a <Text> is silently dropped: it renders a bare <div>, whose role is
|
|
215
|
+
// `generic`, on which aria-label is prohibited by ARIA and ignored by every major
|
|
216
|
+
// browser. The em dash beside it is aria-hidden. So an unavailable metric announced as
|
|
217
|
+
// an EMPTY NODE — a blank cell — which is exactly the "absent must never look like a
|
|
218
|
+
// measured zero" invariant this whole type exists to enforce, inverted for anyone not
|
|
219
|
+
// looking at the screen. The tooltip was the only other route and it hangs off a
|
|
220
|
+
// non-focusable element, so it was pointer-only too.
|
|
221
|
+
//
|
|
222
|
+
// Delta already does this correctly; this is that pattern applied.
|
|
223
|
+
/* @__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.jsxs)(import_sanity_ui_compat.Text, { size, muted: true, children: [
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2014" }),
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: visuallyHidden, children: [
|
|
226
|
+
label,
|
|
227
|
+
": unavailable. ",
|
|
228
|
+
detail
|
|
229
|
+
] })
|
|
230
|
+
] }) })
|
|
231
|
+
);
|
|
211
232
|
}
|
|
212
233
|
const formatted = unit === "percent" ? `${metric.value.toFixed(1)}%` : formatCount(metric.value);
|
|
213
234
|
if (metric.status === "partial") {
|
|
214
235
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
215
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
236
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size, children: [
|
|
237
|
+
formatted,
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: visuallyHidden, children: [
|
|
239
|
+
", partial. ",
|
|
240
|
+
metric.note
|
|
241
|
+
] })
|
|
242
|
+
] }),
|
|
216
243
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: "Partial" })
|
|
217
244
|
] });
|
|
218
245
|
}
|
|
219
246
|
if (metric.status === "estimated") {
|
|
220
247
|
const range = unit === "percent" ? `${metric.low.toFixed(1)}% to ${metric.high.toFixed(1)}%` : `${formatCount(metric.low)} to ${formatCount(metric.high)}`;
|
|
221
248
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
222
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size,
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size, children: [
|
|
223
250
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "~" }),
|
|
224
|
-
formatted
|
|
251
|
+
formatted,
|
|
252
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: visuallyHidden, children: [
|
|
253
|
+
"estimated, between ",
|
|
254
|
+
range,
|
|
255
|
+
". ",
|
|
256
|
+
metric.basis
|
|
257
|
+
] })
|
|
225
258
|
] }),
|
|
226
259
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: range }),
|
|
227
260
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "primary", fontSize: 0, children: "Estimated" })
|
|
@@ -231,7 +264,7 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
|
|
|
231
264
|
const unhandled = metric;
|
|
232
265
|
void unhandled;
|
|
233
266
|
}
|
|
234
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size,
|
|
267
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, children: formatted });
|
|
235
268
|
}
|
|
236
269
|
function ComparisonBar({ label, metric, max, tone = "default" }) {
|
|
237
270
|
const value = metric.status === "unavailable" ? null : metric.value;
|
|
@@ -256,7 +289,7 @@ function ComparisonBar({ label, metric, max, tone = "default" }) {
|
|
|
256
289
|
) : (
|
|
257
290
|
// Track and fill are both Cards so the palette comes from the Studio theme tokens
|
|
258
291
|
// rather than hand-picked CSS variables, and follows light/dark without extra work.
|
|
259
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
292
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: barTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${width}%` } }) })
|
|
260
293
|
)
|
|
261
294
|
] });
|
|
262
295
|
}
|
|
@@ -288,7 +321,7 @@ function ProportionChart({ bars, format, totalLabel }) {
|
|
|
288
321
|
] })
|
|
289
322
|
] })
|
|
290
323
|
] }),
|
|
291
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
324
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: barTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${Math.max(1, share * 100)}%` } }) })
|
|
292
325
|
] }, bar.key);
|
|
293
326
|
}),
|
|
294
327
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
@@ -340,7 +373,7 @@ function FunnelChart({ stages, measurement }) {
|
|
|
340
373
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: stage.label }),
|
|
341
374
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: formatCount(stage.value) })
|
|
342
375
|
] }),
|
|
343
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
376
|
+
/* @__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}%` } }) }),
|
|
344
377
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
345
378
|
index === 0 ? "entry step" : `${formatPercent(share, 1)} of ${stages[0]?.label.toLowerCase()}`,
|
|
346
379
|
index > 1 && stage.conversionFromPrevious !== null && previous ? ` \xB7 ${formatPercent(stage.conversionFromPrevious, 1)} of ${previous.label.toLowerCase()}` : ""
|
|
@@ -374,7 +407,21 @@ function funnelStage(active) {
|
|
|
374
407
|
cursor: "default"
|
|
375
408
|
};
|
|
376
409
|
}
|
|
377
|
-
var
|
|
410
|
+
var barFill = {
|
|
411
|
+
height: "100%",
|
|
412
|
+
borderRadius: 2,
|
|
413
|
+
// currentColor at an alpha that clears 3:1 against the card in both themes, so the mark works
|
|
414
|
+
// from one declaration rather than needing a palette.
|
|
415
|
+
background: "currentColor",
|
|
416
|
+
opacity: 0.55
|
|
417
|
+
};
|
|
418
|
+
var barTrack = {
|
|
419
|
+
height: 8,
|
|
420
|
+
borderRadius: 2,
|
|
421
|
+
overflow: "hidden",
|
|
422
|
+
background: "currentColor",
|
|
423
|
+
opacity: 0.12
|
|
424
|
+
};
|
|
378
425
|
var chartFrame = { position: "relative", width: "100%" };
|
|
379
426
|
var readout = {
|
|
380
427
|
position: "absolute",
|
|
@@ -748,21 +795,23 @@ function SortableTable({
|
|
|
748
795
|
const content = column.render(row);
|
|
749
796
|
return index === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "row", style: bodyCell, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: firstCell, children: [
|
|
750
797
|
content,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
798
|
+
exportName || filterOn ? (
|
|
799
|
+
// Per-row exclusion, because the fix for a contaminated
|
|
800
|
+
// table is to take the bad row out and see what the
|
|
801
|
+
// rest looks like. Rendered on every row rather than
|
|
802
|
+
// on hover so it is reachable by keyboard and touch.
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
804
|
+
"button",
|
|
805
|
+
{
|
|
806
|
+
type: "button",
|
|
807
|
+
style: excludeButton,
|
|
808
|
+
"aria-label": `Exclude ${key}`,
|
|
809
|
+
title: `Exclude ${key} from this table`,
|
|
810
|
+
onClick: () => setExcluded((current) => new Set(current).add(key)),
|
|
811
|
+
children: "\xD7"
|
|
812
|
+
}
|
|
813
|
+
)
|
|
814
|
+
) : null
|
|
766
815
|
] }) }, column.key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: column.numeric ? bodyCellNumeric : bodyCell, children: content }, column.key);
|
|
767
816
|
}) }, key);
|
|
768
817
|
}),
|
|
@@ -898,11 +947,13 @@ var import_d3_scale = require("d3-scale");
|
|
|
898
947
|
var import_d3_shape = require("d3-shape");
|
|
899
948
|
var import_d3_array = require("d3-array");
|
|
900
949
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
950
|
+
var WIDTH = 760;
|
|
901
951
|
var ROW_HEIGHT = 74;
|
|
902
952
|
var AXIS_HEIGHT = 26;
|
|
903
953
|
var GUTTER = 52;
|
|
904
954
|
var RIGHT_PAD = 12;
|
|
905
|
-
var TOP_PAD =
|
|
955
|
+
var TOP_PAD = 22;
|
|
956
|
+
var AXIS_TYPE = 11;
|
|
906
957
|
function formatValue(value, unit, currency) {
|
|
907
958
|
if (unit === "money") return formatMoney(value, currency ?? null);
|
|
908
959
|
if (unit === "percent") return formatPercent(value, 1);
|
|
@@ -920,8 +971,7 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
920
971
|
for (const s of series) for (const p of s.points) all.add(p.date);
|
|
921
972
|
return [...all].sort();
|
|
922
973
|
}, [series]);
|
|
923
|
-
const
|
|
924
|
-
const plotWidth = width - GUTTER - RIGHT_PAD;
|
|
974
|
+
const plotWidth = WIDTH - GUTTER - RIGHT_PAD;
|
|
925
975
|
const height = TOP_PAD + series.length * ROW_HEIGHT + AXIS_HEIGHT;
|
|
926
976
|
const x = (0, import_react3.useMemo)(() => {
|
|
927
977
|
if (dates.length === 0) return null;
|
|
@@ -932,7 +982,7 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
932
982
|
const frame = frameRef.current;
|
|
933
983
|
if (!frame || dates.length === 0) return;
|
|
934
984
|
const box = frame.getBoundingClientRect();
|
|
935
|
-
const ratio = (event.clientX - box.left) / box.width *
|
|
985
|
+
const ratio = (event.clientX - box.left) / box.width * WIDTH;
|
|
936
986
|
const clamped = Math.max(GUTTER, Math.min(GUTTER + plotWidth, ratio));
|
|
937
987
|
const index = Math.round((clamped - GUTTER) / plotWidth * (dates.length - 1));
|
|
938
988
|
setHoverIndex(Math.max(0, Math.min(dates.length - 1, index)));
|
|
@@ -942,52 +992,94 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
942
992
|
if (dates.length < 3 || series.length === 0 || !x) return null;
|
|
943
993
|
const hoveredDate = hoverIndex !== null ? dates[hoverIndex] : null;
|
|
944
994
|
const revealed = pinned || hoverIndex !== null;
|
|
995
|
+
const shapeOf = (row) => {
|
|
996
|
+
const real = row.points.filter((p) => p.value !== null);
|
|
997
|
+
if (real.length === 0) return `${row.label}: no data`;
|
|
998
|
+
const top = real.reduce((best, p) => p.value > best.value ? p : best, real[0]);
|
|
999
|
+
const total = real.reduce((sum, p) => sum + p.value, 0);
|
|
1000
|
+
return `${row.label} (${row.source}): ${formatValue(total, row.unit, currency)} in total, peaking at ${formatValue(top.value, row.unit, currency)} on ${tickLabel(/* @__PURE__ */ new Date(`${top.date}T00:00:00Z`))}`;
|
|
1001
|
+
};
|
|
1002
|
+
const missed = series.flatMap((row) => {
|
|
1003
|
+
if (!row.shortfall) return [];
|
|
1004
|
+
const seen = new Map(row.shortfall.points.map((p) => [p.date, p.value]));
|
|
1005
|
+
let gap = 0;
|
|
1006
|
+
let worst = null;
|
|
1007
|
+
for (const point of row.points) {
|
|
1008
|
+
if (point.value === null) continue;
|
|
1009
|
+
const saw = seen.get(point.date);
|
|
1010
|
+
if (saw == null) continue;
|
|
1011
|
+
gap += point.value - saw;
|
|
1012
|
+
const ratio = point.value > 0 ? 1 - saw / point.value : 0;
|
|
1013
|
+
if (!worst || ratio > worst.ratio) worst = { date: point.date, ratio };
|
|
1014
|
+
}
|
|
1015
|
+
if (gap <= 0 || !worst) return [];
|
|
1016
|
+
return [`${row.shortfall.source} missed ${formatCount(Math.round(gap))} ${row.label.toLowerCase()} over this period; the gap was widest on ${tickLabel(/* @__PURE__ */ new Date(`${worst.date}T00:00:00Z`))} at ${formatPercent(worst.ratio, 0)}.`];
|
|
1017
|
+
});
|
|
1018
|
+
const summary = `${dates.length} days from ${tickLabel(/* @__PURE__ */ new Date(`${dates[0]}T00:00:00Z`))} to ${tickLabel(/* @__PURE__ */ new Date(`${dates[dates.length - 1]}T00:00:00Z`))}. ` + series.map(shapeOf).join(". ") + ". " + missed.join(" ") + (markers.length > 0 ? ` ${markers.length} campaign send${markers.length === 1 ? "" : "s"} marked.` : "");
|
|
945
1019
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
946
1020
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: frameStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
947
1021
|
"svg",
|
|
948
1022
|
{
|
|
949
1023
|
ref: frameRef,
|
|
950
|
-
viewBox: `0 0 ${
|
|
951
|
-
|
|
952
|
-
style: { width: "100%", height: series.length * ROW_HEIGHT + AXIS_HEIGHT + TOP_PAD, display: "block" },
|
|
1024
|
+
viewBox: `0 0 ${WIDTH} ${height}`,
|
|
1025
|
+
style: { width: "100%", height: "auto", display: "block" },
|
|
953
1026
|
onMouseMove: onMove,
|
|
954
1027
|
onMouseLeave: () => setHoverIndex(null),
|
|
1028
|
+
tabIndex: 0,
|
|
955
1029
|
role: "img",
|
|
956
|
-
"aria-label":
|
|
1030
|
+
"aria-label": summary,
|
|
1031
|
+
onFocus: () => setHoverIndex((current) => current ?? dates.length - 1),
|
|
1032
|
+
onBlur: () => setHoverIndex(null),
|
|
1033
|
+
onKeyDown: (event) => {
|
|
1034
|
+
let next = null;
|
|
1035
|
+
const current = hoverIndex ?? dates.length - 1;
|
|
1036
|
+
if (event.key === "ArrowRight") next = Math.min(current + 1, dates.length - 1);
|
|
1037
|
+
if (event.key === "ArrowLeft") next = Math.max(current - 1, 0);
|
|
1038
|
+
if (event.key === "Home") next = 0;
|
|
1039
|
+
if (event.key === "End") next = dates.length - 1;
|
|
1040
|
+
if (event.key === "Escape") {
|
|
1041
|
+
setHoverIndex(null);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (next === null) return;
|
|
1045
|
+
event.preventDefault();
|
|
1046
|
+
setHoverIndex(next);
|
|
1047
|
+
},
|
|
957
1048
|
children: [
|
|
958
1049
|
series.map((row, rowIndex) => {
|
|
959
1050
|
const top = TOP_PAD + rowIndex * ROW_HEIGHT;
|
|
960
1051
|
const bottom = top + ROW_HEIGHT - 18;
|
|
961
1052
|
const values = row.points.map((p) => p.value).filter((v) => v !== null);
|
|
962
1053
|
const peak = (0, import_d3_array.max)(values) ?? 0;
|
|
963
|
-
const
|
|
964
|
-
const y = (0, import_d3_scale.scaleLinear)().domain([0, grossed || 1]).nice().range([bottom, top]);
|
|
1054
|
+
const y = (0, import_d3_scale.scaleLinear)().domain([0, peak || 1]).nice().range([bottom, top]);
|
|
965
1055
|
const at = (p) => x(/* @__PURE__ */ new Date(`${p.date}T00:00:00Z`));
|
|
966
1056
|
const defined = (p) => p.value !== null;
|
|
967
1057
|
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveMonotoneX);
|
|
968
1058
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
969
1059
|
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
970
1060
|
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.curveMonotoneX);
|
|
971
|
-
const bandGen = (0, import_d3_shape.area)().defined(defined).x(at).y0((p) => y(p.value)).y1((p) => y(p.value * row.grossUpFactor)).curve(import_d3_shape.curveMonotoneX);
|
|
972
1061
|
const path = lineGen(row.points) ?? "";
|
|
973
1062
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
974
|
-
const band = row.grossUpFactor && row.grossUpFactor > 1 ? bandGen(row.points) ?? "" : "";
|
|
975
1063
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints.filter((p) => p.value !== null)) ?? "" : "";
|
|
976
1064
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
977
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: bottom, y2: bottom, stroke: "currentColor", strokeWidth:
|
|
978
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER
|
|
979
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1065
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: bottom, y2: bottom, stroke: "currentColor", strokeWidth: 1, opacity: 0.45 }),
|
|
1066
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER, y: top - 6, fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.75, fontWeight: 500, children: row.label }),
|
|
1067
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("text", { x: GUTTER + plotWidth, y: top - 6, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.55, children: [
|
|
1068
|
+
row.source,
|
|
1069
|
+
row.complete ? "" : " \xB7 partial"
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__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) }),
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: bottom + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: "0" }),
|
|
1073
|
+
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: revealed ? 0.34 : 0.26 }),
|
|
1074
|
+
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
983
1075
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
984
1076
|
"path",
|
|
985
1077
|
{
|
|
986
1078
|
d: path,
|
|
987
1079
|
fill: "none",
|
|
988
1080
|
stroke: "currentColor",
|
|
989
|
-
strokeWidth:
|
|
990
|
-
strokeDasharray: row.complete ? void 0 : "
|
|
1081
|
+
strokeWidth: 2,
|
|
1082
|
+
strokeDasharray: row.complete ? void 0 : "6 4",
|
|
991
1083
|
opacity: row.complete ? 0.95 : 0.7
|
|
992
1084
|
}
|
|
993
1085
|
)
|
|
@@ -1005,12 +1097,12 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
1005
1097
|
y1: TOP_PAD - 6,
|
|
1006
1098
|
y2: TOP_PAD + series.length * ROW_HEIGHT - 18,
|
|
1007
1099
|
stroke: "currentColor",
|
|
1008
|
-
strokeWidth:
|
|
1009
|
-
strokeDasharray: "
|
|
1100
|
+
strokeWidth: 1.25,
|
|
1101
|
+
strokeDasharray: "4 3",
|
|
1010
1102
|
opacity: 0.5
|
|
1011
1103
|
}
|
|
1012
1104
|
),
|
|
1013
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: at, cy: TOP_PAD - 8, r:
|
|
1105
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: at, cy: TOP_PAD - 8, r: 3, fill: "currentColor", opacity: 0.7 })
|
|
1014
1106
|
] }, `${marker.date}-${marker.label}`);
|
|
1015
1107
|
}),
|
|
1016
1108
|
hoveredDate && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -1021,7 +1113,7 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
1021
1113
|
y1: TOP_PAD - 6,
|
|
1022
1114
|
y2: TOP_PAD + series.length * ROW_HEIGHT - 18,
|
|
1023
1115
|
stroke: "currentColor",
|
|
1024
|
-
strokeWidth:
|
|
1116
|
+
strokeWidth: 1,
|
|
1025
1117
|
opacity: 0.8
|
|
1026
1118
|
}
|
|
1027
1119
|
),
|
|
@@ -1034,7 +1126,7 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
1034
1126
|
x: x(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)),
|
|
1035
1127
|
y: height - 6,
|
|
1036
1128
|
textAnchor: position === 0 ? "start" : position === 2 ? "end" : "middle",
|
|
1037
|
-
fontSize:
|
|
1129
|
+
fontSize: AXIS_TYPE,
|
|
1038
1130
|
fill: "currentColor",
|
|
1039
1131
|
opacity: 0.55,
|
|
1040
1132
|
children: tickLabel(/* @__PURE__ */ new Date(`${date}T00:00:00Z`))
|
|
@@ -1090,8 +1182,13 @@ function CrossSourceTimeline({ series, markers = [], currency }) {
|
|
|
1090
1182
|
row.source,
|
|
1091
1183
|
")"
|
|
1092
1184
|
] }, row.key)),
|
|
1093
|
-
|
|
1094
|
-
|
|
1185
|
+
missed.map((sentence) => (
|
|
1186
|
+
// The region's argument, stated as a number. It was drawn to scale and described
|
|
1187
|
+
// in the abstract, so the two facts it exists to convey — how much was missed and
|
|
1188
|
+
// when it was worst — were available only by looking hard at a pale fill.
|
|
1189
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: sentence }, sentence)
|
|
1190
|
+
)),
|
|
1191
|
+
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "The shaded area is what your analytics did not see \u2014 a quantity, not a margin of error." }),
|
|
1095
1192
|
markers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "Vertical rules mark campaign sends." })
|
|
1096
1193
|
] })
|
|
1097
1194
|
] });
|
|
@@ -1203,7 +1300,11 @@ function MeasurementHealthPanel({ data, previous }) {
|
|
|
1203
1300
|
shortfall: {
|
|
1204
1301
|
label: "Seen by GA4",
|
|
1205
1302
|
source: "GA4",
|
|
1206
|
-
|
|
1303
|
+
// PAGEVIEWS, not sessions. The region encodes the difference between
|
|
1304
|
+
// these two as an area, so both sides must be the same unit — this
|
|
1305
|
+
// previously shaded sessions under a pageview line, overstating the
|
|
1306
|
+
// loss by the pageviews-per-session ratio.
|
|
1307
|
+
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.ga4Pageviews }))
|
|
1207
1308
|
}
|
|
1208
1309
|
},
|
|
1209
1310
|
...data.crossSource?.some((d) => d.revenue !== null) ? [{
|
|
@@ -1519,10 +1620,11 @@ function JourneyPanel({ data }) {
|
|
|
1519
1620
|
key: "path",
|
|
1520
1621
|
label: "Page",
|
|
1521
1622
|
sortValue: (page) => page.path,
|
|
1522
|
-
// The
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1623
|
+
// The comment here used to describe this fix without applying it: it said
|
|
1624
|
+
// the foundry's own URLs were the one table that did not link while
|
|
1625
|
+
// referrer hosts did, and then rendered plain text. A comment asserting
|
|
1626
|
+
// something the code does not do is worse than no comment.
|
|
1627
|
+
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: page.path, target: "_blank", rel: "noopener noreferrer", style: sourceLink, children: page.path })
|
|
1526
1628
|
},
|
|
1527
1629
|
{
|
|
1528
1630
|
key: "sessions",
|
|
@@ -1957,7 +2059,7 @@ function ReportPanel({
|
|
|
1957
2059
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 4, radius: 2, tone: state.disabled ? "transparent" : "critical", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 3, children: [
|
|
1958
2060
|
state.disabled && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, weight: "medium", children: "Switched off" }),
|
|
1959
2061
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: state.disabled, children: state.message }),
|
|
1960
|
-
!state.disabled && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2062
|
+
!state.disabled && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: rangeButton(false), onClick: reload, children: "Try again" }) })
|
|
1961
2063
|
] }) }),
|
|
1962
2064
|
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
1963
2065
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
@@ -1992,6 +2094,38 @@ function isoDaysAgo(days) {
|
|
|
1992
2094
|
return date.toISOString().slice(0, 10);
|
|
1993
2095
|
}
|
|
1994
2096
|
var COMPARED_REPORTS = ["acquisition", "measurement-health"];
|
|
2097
|
+
var PanelBoundary = class extends import_react4.default.Component {
|
|
2098
|
+
constructor(props) {
|
|
2099
|
+
super(props);
|
|
2100
|
+
this.state = { error: null };
|
|
2101
|
+
}
|
|
2102
|
+
static getDerivedStateFromError(error) {
|
|
2103
|
+
return { error };
|
|
2104
|
+
}
|
|
2105
|
+
componentDidCatch(error) {
|
|
2106
|
+
console.error("Visitor insights: panel render failed:", error.message);
|
|
2107
|
+
}
|
|
2108
|
+
render() {
|
|
2109
|
+
if (!this.state.error) return this.props.children;
|
|
2110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 4, radius: 2, tone: "critical", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 3, children: [
|
|
2111
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, weight: "medium", children: "This panel could not be drawn" }),
|
|
2112
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, children: "The other panels are unaffected. This usually means the site\u2019s API route is a different version from this Studio \u2014 redeploying the site normally clears it." }),
|
|
2113
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: this.state.error.message }),
|
|
2114
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2115
|
+
"button",
|
|
2116
|
+
{
|
|
2117
|
+
type: "button",
|
|
2118
|
+
style: rangeButton(false),
|
|
2119
|
+
onClick: () => {
|
|
2120
|
+
this.setState({ error: null });
|
|
2121
|
+
this.props.onRetry();
|
|
2122
|
+
},
|
|
2123
|
+
children: "Try again"
|
|
2124
|
+
}
|
|
2125
|
+
) })
|
|
2126
|
+
] }) });
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
1995
2129
|
function VisitorInsightsTool(props) {
|
|
1996
2130
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
1997
2131
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
@@ -2019,7 +2153,7 @@ function VisitorInsightsTool(props) {
|
|
|
2019
2153
|
style: { position: "relative" },
|
|
2020
2154
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2021
2155
|
active && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: active.blurb }),
|
|
2022
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ReportPanel, { report: activePanel, apiBaseUrl, range, custom })
|
|
2156
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanelBoundary, { onRetry: () => setActivePanel(activePanel), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ReportPanel, { report: activePanel, apiBaseUrl, range, custom }) })
|
|
2023
2157
|
] })
|
|
2024
2158
|
}
|
|
2025
2159
|
)
|