@liiift-studio/sanity-visitor-insights 0.22.0 → 0.24.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 +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +149 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -43
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +2 -1
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/capture.ts +9 -1
- package/src/index.ts +1 -1
- package/src/studio/CrossSourceTimeline.tsx +195 -16
- package/src/studio/Figure.tsx +16 -2
- package/src/studio/VisitorInsightsTool.tsx +81 -19
- package/src/studio/panels.test.tsx +299 -2
- package/src/studio/panels.tsx +41 -8
- package/src/studio/useReport.ts +53 -0
package/dist/index.d.mts
CHANGED
|
@@ -43,6 +43,20 @@ declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps):
|
|
|
43
43
|
* extractable.
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* What is known about GA4's coverage of this site and window, or null when nothing is.
|
|
48
|
+
*
|
|
49
|
+
* Null is a real answer and must render as nothing: a panel that cannot say how lossy its source is
|
|
50
|
+
* should not imply the source is fine.
|
|
51
|
+
*
|
|
52
|
+
* @param base - the site's API base URL, as passed to the tool
|
|
53
|
+
* @param range - the range name currently selected
|
|
54
|
+
* @param custom - the custom bounds, when the range is custom
|
|
55
|
+
*/
|
|
56
|
+
declare function knownShortfall(base: string, range: string, custom?: {
|
|
57
|
+
start: string;
|
|
58
|
+
end: string;
|
|
59
|
+
}): number | null;
|
|
46
60
|
/** Loading state for a report. */
|
|
47
61
|
type ReportState<T> = {
|
|
48
62
|
status: 'idle';
|
|
@@ -399,6 +413,16 @@ interface Series {
|
|
|
399
413
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
400
414
|
complete: boolean;
|
|
401
415
|
unit: SeriesUnit;
|
|
416
|
+
/**
|
|
417
|
+
* How the row is drawn. Defaults to `line`.
|
|
418
|
+
*
|
|
419
|
+
* `events` is for a quantity that only exists on the days it happened — orders, revenue. Darden
|
|
420
|
+
* does seven orders a quarter, and a monotone curve through those seven points drew a smooth
|
|
421
|
+
* rise and fall across eighty-three days on which nothing occurred, with the curve's overshoot
|
|
422
|
+
* putting non-zero revenue on days that had none. A stem per day says what a sale is: a discrete
|
|
423
|
+
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
424
|
+
*/
|
|
425
|
+
mark?: 'line' | 'events';
|
|
402
426
|
points: SeriesPoint[];
|
|
403
427
|
/**
|
|
404
428
|
* What a lossier source saw of the same thing.
|
|
@@ -552,4 +576,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
552
576
|
*/
|
|
553
577
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
554
578
|
|
|
555
|
-
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, useReport, visitorInsights };
|
|
579
|
+
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, knownShortfall, useReport, visitorInsights };
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,20 @@ declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps):
|
|
|
43
43
|
* extractable.
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* What is known about GA4's coverage of this site and window, or null when nothing is.
|
|
48
|
+
*
|
|
49
|
+
* Null is a real answer and must render as nothing: a panel that cannot say how lossy its source is
|
|
50
|
+
* should not imply the source is fine.
|
|
51
|
+
*
|
|
52
|
+
* @param base - the site's API base URL, as passed to the tool
|
|
53
|
+
* @param range - the range name currently selected
|
|
54
|
+
* @param custom - the custom bounds, when the range is custom
|
|
55
|
+
*/
|
|
56
|
+
declare function knownShortfall(base: string, range: string, custom?: {
|
|
57
|
+
start: string;
|
|
58
|
+
end: string;
|
|
59
|
+
}): number | null;
|
|
46
60
|
/** Loading state for a report. */
|
|
47
61
|
type ReportState<T> = {
|
|
48
62
|
status: 'idle';
|
|
@@ -399,6 +413,16 @@ interface Series {
|
|
|
399
413
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
400
414
|
complete: boolean;
|
|
401
415
|
unit: SeriesUnit;
|
|
416
|
+
/**
|
|
417
|
+
* How the row is drawn. Defaults to `line`.
|
|
418
|
+
*
|
|
419
|
+
* `events` is for a quantity that only exists on the days it happened — orders, revenue. Darden
|
|
420
|
+
* does seven orders a quarter, and a monotone curve through those seven points drew a smooth
|
|
421
|
+
* rise and fall across eighty-three days on which nothing occurred, with the curve's overshoot
|
|
422
|
+
* putting non-zero revenue on days that had none. A stem per day says what a sale is: a discrete
|
|
423
|
+
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
424
|
+
*/
|
|
425
|
+
mark?: 'line' | 'events';
|
|
402
426
|
points: SeriesPoint[];
|
|
403
427
|
/**
|
|
404
428
|
* What a lossier source saw of the same thing.
|
|
@@ -552,4 +576,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
552
576
|
*/
|
|
553
577
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
554
578
|
|
|
555
|
-
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, useReport, visitorInsights };
|
|
579
|
+
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, knownShortfall, useReport, visitorInsights };
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
formatMoney: () => formatMoney,
|
|
58
58
|
formatPercent: () => formatPercent,
|
|
59
59
|
isReportName: () => isReportName,
|
|
60
|
+
knownShortfall: () => knownShortfall,
|
|
60
61
|
ok: () => ok,
|
|
61
62
|
partial: () => partial,
|
|
62
63
|
previousRange: () => previousRange,
|
|
@@ -79,6 +80,19 @@ var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
|
|
|
79
80
|
var import_react = require("react");
|
|
80
81
|
var import_sanity = require("sanity");
|
|
81
82
|
var envelopeCache = /* @__PURE__ */ new Map();
|
|
83
|
+
var shortfallByWindow = /* @__PURE__ */ new Map();
|
|
84
|
+
function windowKey(range, custom) {
|
|
85
|
+
return range === "custom" && custom ? `${custom.start}..${custom.end}` : range;
|
|
86
|
+
}
|
|
87
|
+
function knownShortfall(base, range, custom) {
|
|
88
|
+
return shortfallByWindow.get(`${base}|${windowKey(range, custom)}`) ?? null;
|
|
89
|
+
}
|
|
90
|
+
function rememberShortfall(base, range, custom, data) {
|
|
91
|
+
const ratio = data?.shortfallRatio;
|
|
92
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio >= 0 && ratio <= 1) {
|
|
93
|
+
shortfallByWindow.set(`${base}|${windowKey(range, custom)}`, ratio);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
82
96
|
function cacheKey(base, report, range, custom) {
|
|
83
97
|
return [base, report, range, custom?.start ?? "", custom?.end ?? ""].join("|");
|
|
84
98
|
}
|
|
@@ -140,6 +154,7 @@ function useReport({ apiBaseUrl, report, range, custom, enabled = true }) {
|
|
|
140
154
|
const envelope = await response.json();
|
|
141
155
|
if (requestIdRef.current !== requestId) return;
|
|
142
156
|
envelopeCache.set(key, envelope);
|
|
157
|
+
rememberShortfall(apiBaseUrl, range, custom, envelope.data);
|
|
143
158
|
setState({ status: "ready", envelope });
|
|
144
159
|
} catch (e) {
|
|
145
160
|
if (controller.signal.aborted || requestIdRef.current !== requestId) return;
|
|
@@ -827,7 +842,8 @@ function SortableTable({
|
|
|
827
842
|
copyTimer.current = window.setTimeout(() => setCopied("idle"), 4e3);
|
|
828
843
|
}
|
|
829
844
|
};
|
|
830
|
-
const
|
|
845
|
+
const excludedInWindow = rows.filter((row) => excluded.has(rowKey(row))).length;
|
|
846
|
+
const hiddenCount = rows.length - visible.length - excludedInWindow;
|
|
831
847
|
const excludedCount = excluded.size;
|
|
832
848
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
833
849
|
(filterOn || exportName) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: tableControls, children: [
|
|
@@ -905,7 +921,7 @@ function SortableTable({
|
|
|
905
921
|
] }) }, column.key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: column.numeric ? bodyCellNumeric : bodyCell, children: content }, column.key);
|
|
906
922
|
}) }, key);
|
|
907
923
|
}),
|
|
908
|
-
ordered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { colSpan: columns.length, style: bodyCell, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: "No rows match this filter." }) }) })
|
|
924
|
+
ordered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { colSpan: columns.length, style: bodyCell, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: rows.length === 0 ? "Nothing to show for this period." : query.trim() || excludedInWindow > 0 ? "No rows match this filter." : "Nothing to show for this period." }) }) })
|
|
909
925
|
] })
|
|
910
926
|
] }) }),
|
|
911
927
|
truncatedNote && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: truncatedNote })
|
|
@@ -1031,7 +1047,7 @@ var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
|
|
|
1031
1047
|
var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
|
|
1032
1048
|
|
|
1033
1049
|
// src/studio/CrossSourceTimeline.tsx
|
|
1034
|
-
var import_react3 = require("react");
|
|
1050
|
+
var import_react3 = __toESM(require("react"));
|
|
1035
1051
|
var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
|
|
1036
1052
|
var import_d3_scale = require("d3-scale");
|
|
1037
1053
|
var import_d3_shape = require("d3-shape");
|
|
@@ -1053,18 +1069,48 @@ function formatValue(value, unit, currency) {
|
|
|
1053
1069
|
function tickLabel(date) {
|
|
1054
1070
|
return `${date.getUTCDate()} ${date.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
|
|
1055
1071
|
}
|
|
1072
|
+
function dayIndexAt(clientX, left, boxWidth, viewBoxWidth, plotWidth, count) {
|
|
1073
|
+
if (count === 0 || boxWidth <= 0 || plotWidth <= 0) return null;
|
|
1074
|
+
const ratio = (clientX - left) / boxWidth * viewBoxWidth;
|
|
1075
|
+
const clamped = Math.max(GUTTER, Math.min(GUTTER + plotWidth, ratio));
|
|
1076
|
+
const index = Math.round((clamped - GUTTER) / plotWidth * (count - 1));
|
|
1077
|
+
return Math.max(0, Math.min(count - 1, index));
|
|
1078
|
+
}
|
|
1056
1079
|
function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
1057
1080
|
const [hoverIndex, setHoverIndex] = (0, import_react3.useState)(null);
|
|
1058
1081
|
const [pinned, setPinned] = (0, import_react3.useState)(false);
|
|
1059
1082
|
const [brushAnchor, setBrushAnchor] = (0, import_react3.useState)(null);
|
|
1060
1083
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1084
|
+
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
1061
1085
|
const frameRef = (0, import_react3.useRef)(null);
|
|
1086
|
+
const [frameNode, setFrameNode] = (0, import_react3.useState)(null);
|
|
1087
|
+
const attachFrame = (0, import_react3.useCallback)((node) => {
|
|
1088
|
+
frameRef.current = node;
|
|
1089
|
+
setFrameNode(node);
|
|
1090
|
+
}, []);
|
|
1062
1091
|
const dates = (0, import_react3.useMemo)(() => {
|
|
1063
1092
|
const all = /* @__PURE__ */ new Set();
|
|
1064
1093
|
for (const s of series) for (const p of s.points) all.add(p.date);
|
|
1065
1094
|
return [...all].sort();
|
|
1066
1095
|
}, [series]);
|
|
1067
|
-
const plotWidth =
|
|
1096
|
+
const plotWidth = Math.max(120, measured - GUTTER - RIGHT_PAD);
|
|
1097
|
+
import_react3.default.useLayoutEffect(() => {
|
|
1098
|
+
const frame = frameNode;
|
|
1099
|
+
if (!frame) return;
|
|
1100
|
+
const apply = (width) => {
|
|
1101
|
+
if (width <= 0) return;
|
|
1102
|
+
const next = Math.round(width);
|
|
1103
|
+
setMeasured((current) => Math.abs(next - current) > 1 ? next : current);
|
|
1104
|
+
};
|
|
1105
|
+
apply(frame.getBoundingClientRect().width);
|
|
1106
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
1107
|
+
const observer = new ResizeObserver((entries) => {
|
|
1108
|
+
const width = entries[0]?.contentRect.width;
|
|
1109
|
+
if (width !== void 0) apply(width);
|
|
1110
|
+
});
|
|
1111
|
+
observer.observe(frame);
|
|
1112
|
+
return () => observer.disconnect();
|
|
1113
|
+
}, [frameNode]);
|
|
1068
1114
|
const height = TOP_PAD + series.length * ROW_HEIGHT + AXIS_HEIGHT;
|
|
1069
1115
|
const x = (0, import_react3.useMemo)(() => {
|
|
1070
1116
|
if (dates.length === 0) return null;
|
|
@@ -1075,12 +1121,9 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1075
1121
|
const frame = frameRef.current;
|
|
1076
1122
|
if (!frame || dates.length === 0) return null;
|
|
1077
1123
|
const box = frame.getBoundingClientRect();
|
|
1078
|
-
|
|
1079
|
-
const clamped = Math.max(GUTTER, Math.min(GUTTER + plotWidth, ratio));
|
|
1080
|
-
const index = Math.round((clamped - GUTTER) / plotWidth * (dates.length - 1));
|
|
1081
|
-
return Math.max(0, Math.min(dates.length - 1, index));
|
|
1124
|
+
return dayIndexAt(clientX, box.left, box.width, measured, plotWidth, dates.length);
|
|
1082
1125
|
},
|
|
1083
|
-
[dates.length, plotWidth]
|
|
1126
|
+
[dates.length, plotWidth, measured]
|
|
1084
1127
|
);
|
|
1085
1128
|
const commit = (0, import_react3.useCallback)(
|
|
1086
1129
|
(from, to) => {
|
|
@@ -1108,6 +1151,13 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1108
1151
|
if (real.length === 0) return `${row.label}: no data`;
|
|
1109
1152
|
const top = real.reduce((best, p) => p.value > best.value ? p : best, real[0]);
|
|
1110
1153
|
const total = real.reduce((sum, p) => sum + p.value, 0);
|
|
1154
|
+
if (row.mark === "events") {
|
|
1155
|
+
const active = real.filter((p) => p.value !== 0);
|
|
1156
|
+
if (active.length === 0) {
|
|
1157
|
+
return `${row.label} (${row.source}): nothing on any of the ${real.length} days measured`;
|
|
1158
|
+
}
|
|
1159
|
+
return `${row.label} (${row.source}): ${formatValue(total, row.unit, currency)} across ${active.length} of ${real.length} days, the largest ${formatValue(top.value, row.unit, currency)} on ${tickLabel(/* @__PURE__ */ new Date(`${top.date}T00:00:00Z`))}`;
|
|
1160
|
+
}
|
|
1111
1161
|
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`))}`;
|
|
1112
1162
|
};
|
|
1113
1163
|
const missed = series.flatMap((row) => {
|
|
@@ -1131,8 +1181,8 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1131
1181
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: frameStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1132
1182
|
"svg",
|
|
1133
1183
|
{
|
|
1134
|
-
ref:
|
|
1135
|
-
viewBox: `0 0 ${
|
|
1184
|
+
ref: attachFrame,
|
|
1185
|
+
viewBox: `0 0 ${measured} ${height}`,
|
|
1136
1186
|
onMouseMove: onMove,
|
|
1137
1187
|
onMouseLeave: () => {
|
|
1138
1188
|
setHoverIndex(null);
|
|
@@ -1160,7 +1210,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1160
1210
|
setBrushed(null);
|
|
1161
1211
|
commit(from, to);
|
|
1162
1212
|
},
|
|
1163
|
-
style: { width: "100%", height
|
|
1213
|
+
style: { width: "100%", height, display: "block", cursor: onBrush ? "col-resize" : "default" },
|
|
1164
1214
|
tabIndex: 0,
|
|
1165
1215
|
role: "img",
|
|
1166
1216
|
"aria-label": summary,
|
|
@@ -1205,7 +1255,11 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1205
1255
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
1206
1256
|
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
1207
1257
|
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);
|
|
1208
|
-
const path = lineGen(row.points) ?? "";
|
|
1258
|
+
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
1259
|
+
const pitch = plotWidth / Math.max(1, dates.length - 1);
|
|
1260
|
+
const stemWidth = Math.max(1, Math.min(9, pitch > 2 ? pitch - 1 : pitch));
|
|
1261
|
+
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
1262
|
+
const zeroes = row.mark === "events" ? row.points.filter((p) => p.value === 0) : [];
|
|
1209
1263
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1210
1264
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints.filter((p) => p.value !== null)) ?? "" : "";
|
|
1211
1265
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -1219,7 +1273,40 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1219
1273
|
/* @__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" }),
|
|
1220
1274
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1221
1275
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
1222
|
-
|
|
1276
|
+
zeroes.map((p) => {
|
|
1277
|
+
const cx = at(p);
|
|
1278
|
+
if (!Number.isFinite(cx)) return null;
|
|
1279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1280
|
+
"rect",
|
|
1281
|
+
{
|
|
1282
|
+
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1283
|
+
y: bottom - 1,
|
|
1284
|
+
width: stemWidth,
|
|
1285
|
+
height: 1,
|
|
1286
|
+
fill: "currentColor",
|
|
1287
|
+
opacity: 0.3
|
|
1288
|
+
},
|
|
1289
|
+
`zero-${p.date}`
|
|
1290
|
+
);
|
|
1291
|
+
}),
|
|
1292
|
+
stems.map((p) => {
|
|
1293
|
+
const cx = at(p);
|
|
1294
|
+
const headY = y(p.value);
|
|
1295
|
+
if (!Number.isFinite(cx) || !Number.isFinite(headY)) return null;
|
|
1296
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1297
|
+
"rect",
|
|
1298
|
+
{
|
|
1299
|
+
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1300
|
+
y: Math.min(headY, bottom),
|
|
1301
|
+
width: stemWidth,
|
|
1302
|
+
height: Math.max(1.5, Math.abs(bottom - headY)),
|
|
1303
|
+
fill: "currentColor",
|
|
1304
|
+
opacity: 0.75
|
|
1305
|
+
},
|
|
1306
|
+
p.date
|
|
1307
|
+
);
|
|
1308
|
+
}),
|
|
1309
|
+
path && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1223
1310
|
"path",
|
|
1224
1311
|
{
|
|
1225
1312
|
d: path,
|
|
@@ -1346,7 +1433,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1346
1433
|
}
|
|
1347
1434
|
),
|
|
1348
1435
|
series.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
|
|
1349
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: row.complete ? "\u2500\u2500\u2500" : "\u254C\u254C\u254C" }),
|
|
1436
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: row.mark === "events" ? "\u25AE\u25AE\u25AE" : row.complete ? "\u2500\u2500\u2500" : "\u254C\u254C\u254C" }),
|
|
1350
1437
|
" ",
|
|
1351
1438
|
row.label,
|
|
1352
1439
|
" (",
|
|
@@ -1513,6 +1600,8 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1513
1600
|
source: "Sanity",
|
|
1514
1601
|
complete: true,
|
|
1515
1602
|
unit: "money",
|
|
1603
|
+
// A sale is an event on a date, not a level that varies day to day.
|
|
1604
|
+
mark: "events",
|
|
1516
1605
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.revenue }))
|
|
1517
1606
|
}] : [{
|
|
1518
1607
|
key: "orders",
|
|
@@ -1520,6 +1609,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1520
1609
|
source: "Sanity",
|
|
1521
1610
|
complete: true,
|
|
1522
1611
|
unit: "count",
|
|
1612
|
+
mark: "events",
|
|
1523
1613
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.orders }))
|
|
1524
1614
|
}]
|
|
1525
1615
|
]
|
|
@@ -1631,11 +1721,12 @@ function Verdict({ data, previous }) {
|
|
|
1631
1721
|
const sends = data.campaigns?.length ?? 0;
|
|
1632
1722
|
if (sends > 0) parts.push(`${sends} campaign${sends === 1 ? "" : "s"} sent`);
|
|
1633
1723
|
const shortfall = data.shortfallRatio;
|
|
1634
|
-
const
|
|
1724
|
+
const seeing = shortfall !== null && shortfall !== void 0 ? `GA4 is seeing ${formatPercent(1 - shortfall, 0)} of your traffic` : null;
|
|
1725
|
+
const broken = shortfall === null || shortfall === void 0 ? data.capture?.discrepancy ? "measurement disagrees between sources" : null : shortfall > 0.6 ? `${seeing} \u2014 treat its figures as broken` : shortfall > 0.35 ? `${seeing} \u2014 its figures are undercounts, not what happened` : data.capture?.discrepancy ? "measurement disagrees between sources" : null;
|
|
1635
1726
|
if (broken) parts.push(broken);
|
|
1636
1727
|
if (parts.length === 0) return null;
|
|
1637
|
-
const
|
|
1638
|
-
const closing = broken ? "" :
|
|
1728
|
+
const noticeable = shortfall !== null && shortfall !== void 0 && shortfall > 0.2;
|
|
1729
|
+
const closing = broken || !noticeable ? "" : ` \xB7 ${seeing}`;
|
|
1639
1730
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1640
1731
|
import_sanity_ui_compat3.Card,
|
|
1641
1732
|
{
|
|
@@ -1668,7 +1759,8 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
1668
1759
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "How much GA4 is seeing" }),
|
|
1669
1760
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: (data.capture?.estimates ?? []).map((estimate) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1670
1761
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: estimate.basis === "orders" ? "Measured against orders" : estimate.basis === "email" ? "Measured against email clicks" : "Measured against Vercel" }),
|
|
1671
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatPercent(
|
|
1762
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatPercent(estimate.rate, 0) }),
|
|
1763
|
+
estimate.rate > 1.05 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, children: "Above 100%: GA4 is counting more than the source it is measured against. That is usually a tag firing twice, not extra traffic." }),
|
|
1672
1764
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
|
|
1673
1765
|
formatCount(estimate.observed),
|
|
1674
1766
|
" of ",
|
|
@@ -2145,6 +2237,7 @@ var RANGES = [
|
|
|
2145
2237
|
{ key: "quarter", label: "Quarter", span: "the last 91 days" },
|
|
2146
2238
|
{ key: "year", label: "Year", span: "the last 365 days" }
|
|
2147
2239
|
];
|
|
2240
|
+
var GA4_ONLY_TABS = ["acquisition", "journey", "typeface-interest"];
|
|
2148
2241
|
var PANELS = [
|
|
2149
2242
|
{ id: "overview", report: "measurement-health", label: "Overview", blurb: "Money, traffic and what moved this period" },
|
|
2150
2243
|
{ id: "acquisition", report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
|
|
@@ -2329,6 +2422,17 @@ function PanelTabs({ value, onChange }) {
|
|
|
2329
2422
|
);
|
|
2330
2423
|
}) });
|
|
2331
2424
|
}
|
|
2425
|
+
function CoverageRibbon({ ratio }) {
|
|
2426
|
+
if (ratio === null || ratio < 0.2) return null;
|
|
2427
|
+
const seen = Math.round((1 - ratio) * 100);
|
|
2428
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: ratio > 0.6 ? "critical" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2429
|
+
"Every figure on this tab comes from Google Analytics, which is seeing about ",
|
|
2430
|
+
seen,
|
|
2431
|
+
"% of this site\u2019s traffic in this window. Read them as at least ",
|
|
2432
|
+
seen === 0 ? "undercounts" : `${seen}% of the real numbers`,
|
|
2433
|
+
", not as what happened. Data health explains why."
|
|
2434
|
+
] }) });
|
|
2435
|
+
}
|
|
2332
2436
|
function ReportPanel({
|
|
2333
2437
|
tabId,
|
|
2334
2438
|
report,
|
|
@@ -2352,10 +2456,6 @@ function ReportPanel({
|
|
|
2352
2456
|
// felt state of the tool was "slightly unreadable", and muted text at 55% is under
|
|
2353
2457
|
// 3:1 in both themes. The line says it; the figures stay legible.
|
|
2354
2458
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: "Updating\u2026" }),
|
|
2355
|
-
state.status === "ready" && state.revalidationError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2356
|
-
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2357
|
-
state.revalidationError
|
|
2358
|
-
] }) }),
|
|
2359
2459
|
state.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Flex, { align: "center", gap: 3, padding: 4, children: [
|
|
2360
2460
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Spinner, { muted: true }),
|
|
2361
2461
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Loading\u2026" })
|
|
@@ -2370,33 +2470,38 @@ function ReportPanel({
|
|
|
2370
2470
|
] }) }),
|
|
2371
2471
|
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2372
2472
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
2373
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2473
|
+
state.revalidationError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2474
|
+
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2475
|
+
state.revalidationError
|
|
2476
|
+
] }) }),
|
|
2374
2477
|
state.envelope.comparison && COMPARED_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2375
2478
|
"Changes are against ",
|
|
2376
2479
|
state.envelope.comparison.range.start,
|
|
2377
2480
|
" to ",
|
|
2378
2481
|
state.envelope.comparison.range.end,
|
|
2379
2482
|
", the equivalent window immediately before this one.",
|
|
2380
|
-
state.envelope.comparison.provisional && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: " This window\u2019s last days are still being processed by GA4, so changes read low." })
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2483
|
+
state.envelope.comparison.provisional && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: " This window\u2019s last days are still being processed by GA4, so changes read low." }),
|
|
2484
|
+
GA4_ONLY_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CoverageRibbon, { ratio: knownShortfall(apiBaseUrl, range, range === "custom" ? custom : void 0) }),
|
|
2485
|
+
tabId === "overview" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OverviewPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data, onBrush }),
|
|
2486
|
+
tabId === "data-health" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2487
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2488
|
+
DataHealthPanel,
|
|
2489
|
+
{
|
|
2490
|
+
data: state.envelope.data,
|
|
2491
|
+
diagnostics: diagnostics.status === "ready" ? diagnostics.envelope.data : void 0
|
|
2492
|
+
}
|
|
2493
|
+
),
|
|
2494
|
+
diagnostics.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Checking configuration\u2026" }),
|
|
2495
|
+
diagnostics.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2496
|
+
"Configuration checks could not run: ",
|
|
2497
|
+
diagnostics.message
|
|
2498
|
+
] }) })
|
|
2499
|
+
] }),
|
|
2500
|
+
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data }),
|
|
2501
|
+
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: state.envelope.data }),
|
|
2502
|
+
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
2503
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: state.envelope.notices })
|
|
2396
2504
|
] }),
|
|
2397
|
-
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data }),
|
|
2398
|
-
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: state.envelope.data }),
|
|
2399
|
-
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
2400
2505
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2401
2506
|
"Figures cover ",
|
|
2402
2507
|
state.envelope.range.start,
|
|
@@ -2746,6 +2851,7 @@ var index_default = visitorInsights;
|
|
|
2746
2851
|
formatMoney,
|
|
2747
2852
|
formatPercent,
|
|
2748
2853
|
isReportName,
|
|
2854
|
+
knownShortfall,
|
|
2749
2855
|
ok,
|
|
2750
2856
|
partial,
|
|
2751
2857
|
previousRange,
|