@liiift-studio/sanity-visitor-insights 0.23.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 +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +119 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -47
- 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 +136 -21
- package/src/studio/Figure.tsx +16 -2
- package/src/studio/VisitorInsightsTool.tsx +72 -16
- package/src/studio/panels.test.tsx +222 -30
- package/src/studio/panels.tsx +38 -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';
|
|
@@ -562,4 +576,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
562
576
|
*/
|
|
563
577
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
564
578
|
|
|
565
|
-
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';
|
|
@@ -562,4 +576,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
562
576
|
*/
|
|
563
577
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
564
578
|
|
|
565
|
-
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 })
|
|
@@ -1053,6 +1069,13 @@ 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);
|
|
@@ -1060,22 +1083,34 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1060
1083
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1061
1084
|
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
1062
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
|
+
}, []);
|
|
1063
1091
|
const dates = (0, import_react3.useMemo)(() => {
|
|
1064
1092
|
const all = /* @__PURE__ */ new Set();
|
|
1065
1093
|
for (const s of series) for (const p of s.points) all.add(p.date);
|
|
1066
1094
|
return [...all].sort();
|
|
1067
1095
|
}, [series]);
|
|
1068
1096
|
const plotWidth = Math.max(120, measured - GUTTER - RIGHT_PAD);
|
|
1069
|
-
import_react3.default.
|
|
1070
|
-
const frame =
|
|
1071
|
-
if (!frame
|
|
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;
|
|
1072
1107
|
const observer = new ResizeObserver((entries) => {
|
|
1073
1108
|
const width = entries[0]?.contentRect.width;
|
|
1074
|
-
if (width
|
|
1109
|
+
if (width !== void 0) apply(width);
|
|
1075
1110
|
});
|
|
1076
1111
|
observer.observe(frame);
|
|
1077
1112
|
return () => observer.disconnect();
|
|
1078
|
-
}, [
|
|
1113
|
+
}, [frameNode]);
|
|
1079
1114
|
const height = TOP_PAD + series.length * ROW_HEIGHT + AXIS_HEIGHT;
|
|
1080
1115
|
const x = (0, import_react3.useMemo)(() => {
|
|
1081
1116
|
if (dates.length === 0) return null;
|
|
@@ -1086,12 +1121,9 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1086
1121
|
const frame = frameRef.current;
|
|
1087
1122
|
if (!frame || dates.length === 0) return null;
|
|
1088
1123
|
const box = frame.getBoundingClientRect();
|
|
1089
|
-
|
|
1090
|
-
const clamped = Math.max(GUTTER, Math.min(GUTTER + plotWidth, ratio));
|
|
1091
|
-
const index = Math.round((clamped - GUTTER) / plotWidth * (dates.length - 1));
|
|
1092
|
-
return Math.max(0, Math.min(dates.length - 1, index));
|
|
1124
|
+
return dayIndexAt(clientX, box.left, box.width, measured, plotWidth, dates.length);
|
|
1093
1125
|
},
|
|
1094
|
-
[dates.length, plotWidth]
|
|
1126
|
+
[dates.length, plotWidth, measured]
|
|
1095
1127
|
);
|
|
1096
1128
|
const commit = (0, import_react3.useCallback)(
|
|
1097
1129
|
(from, to) => {
|
|
@@ -1119,6 +1151,13 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1119
1151
|
if (real.length === 0) return `${row.label}: no data`;
|
|
1120
1152
|
const top = real.reduce((best, p) => p.value > best.value ? p : best, real[0]);
|
|
1121
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
|
+
}
|
|
1122
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`))}`;
|
|
1123
1162
|
};
|
|
1124
1163
|
const missed = series.flatMap((row) => {
|
|
@@ -1142,7 +1181,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1142
1181
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: frameStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1143
1182
|
"svg",
|
|
1144
1183
|
{
|
|
1145
|
-
ref:
|
|
1184
|
+
ref: attachFrame,
|
|
1146
1185
|
viewBox: `0 0 ${measured} ${height}`,
|
|
1147
1186
|
onMouseMove: onMove,
|
|
1148
1187
|
onMouseLeave: () => {
|
|
@@ -1217,9 +1256,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1217
1256
|
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
1218
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);
|
|
1219
1258
|
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
1220
|
-
const
|
|
1221
|
-
const stemWidth = Math.max(1
|
|
1222
|
-
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value
|
|
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) : [];
|
|
1223
1263
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1224
1264
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints.filter((p) => p.value !== null)) ?? "" : "";
|
|
1225
1265
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -1233,17 +1273,33 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1233
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" }),
|
|
1234
1274
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1235
1275
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
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
|
+
}),
|
|
1236
1292
|
stems.map((p) => {
|
|
1237
1293
|
const cx = at(p);
|
|
1238
|
-
const
|
|
1239
|
-
if (!Number.isFinite(cx) || !Number.isFinite(
|
|
1294
|
+
const headY = y(p.value);
|
|
1295
|
+
if (!Number.isFinite(cx) || !Number.isFinite(headY)) return null;
|
|
1240
1296
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1241
1297
|
"rect",
|
|
1242
1298
|
{
|
|
1243
|
-
x: cx - stemWidth / 2,
|
|
1244
|
-
y:
|
|
1299
|
+
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1300
|
+
y: Math.min(headY, bottom),
|
|
1245
1301
|
width: stemWidth,
|
|
1246
|
-
height: Math.max(1.5, bottom -
|
|
1302
|
+
height: Math.max(1.5, Math.abs(bottom - headY)),
|
|
1247
1303
|
fill: "currentColor",
|
|
1248
1304
|
opacity: 0.75
|
|
1249
1305
|
},
|
|
@@ -1377,7 +1433,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1377
1433
|
}
|
|
1378
1434
|
),
|
|
1379
1435
|
series.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
|
|
1380
|
-
/* @__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" }),
|
|
1381
1437
|
" ",
|
|
1382
1438
|
row.label,
|
|
1383
1439
|
" (",
|
|
@@ -1665,11 +1721,12 @@ function Verdict({ data, previous }) {
|
|
|
1665
1721
|
const sends = data.campaigns?.length ?? 0;
|
|
1666
1722
|
if (sends > 0) parts.push(`${sends} campaign${sends === 1 ? "" : "s"} sent`);
|
|
1667
1723
|
const shortfall = data.shortfallRatio;
|
|
1668
|
-
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;
|
|
1669
1726
|
if (broken) parts.push(broken);
|
|
1670
1727
|
if (parts.length === 0) return null;
|
|
1671
|
-
const
|
|
1672
|
-
const closing = broken ? "" :
|
|
1728
|
+
const noticeable = shortfall !== null && shortfall !== void 0 && shortfall > 0.2;
|
|
1729
|
+
const closing = broken || !noticeable ? "" : ` \xB7 ${seeing}`;
|
|
1673
1730
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1674
1731
|
import_sanity_ui_compat3.Card,
|
|
1675
1732
|
{
|
|
@@ -1702,7 +1759,8 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
1702
1759
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "How much GA4 is seeing" }),
|
|
1703
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: [
|
|
1704
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" }),
|
|
1705
|
-
/* @__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." }),
|
|
1706
1764
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
|
|
1707
1765
|
formatCount(estimate.observed),
|
|
1708
1766
|
" of ",
|
|
@@ -2179,6 +2237,7 @@ var RANGES = [
|
|
|
2179
2237
|
{ key: "quarter", label: "Quarter", span: "the last 91 days" },
|
|
2180
2238
|
{ key: "year", label: "Year", span: "the last 365 days" }
|
|
2181
2239
|
];
|
|
2240
|
+
var GA4_ONLY_TABS = ["acquisition", "journey", "typeface-interest"];
|
|
2182
2241
|
var PANELS = [
|
|
2183
2242
|
{ id: "overview", report: "measurement-health", label: "Overview", blurb: "Money, traffic and what moved this period" },
|
|
2184
2243
|
{ id: "acquisition", report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
|
|
@@ -2363,6 +2422,17 @@ function PanelTabs({ value, onChange }) {
|
|
|
2363
2422
|
);
|
|
2364
2423
|
}) });
|
|
2365
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
|
+
}
|
|
2366
2436
|
function ReportPanel({
|
|
2367
2437
|
tabId,
|
|
2368
2438
|
report,
|
|
@@ -2399,37 +2469,38 @@ function ReportPanel({
|
|
|
2399
2469
|
!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" }) })
|
|
2400
2470
|
] }) }),
|
|
2401
2471
|
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2402
|
-
|
|
2403
|
-
tabId === "data-health" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2404
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2405
|
-
DataHealthPanel,
|
|
2406
|
-
{
|
|
2407
|
-
data: state.envelope.data,
|
|
2408
|
-
diagnostics: diagnostics.status === "ready" ? diagnostics.envelope.data : void 0
|
|
2409
|
-
}
|
|
2410
|
-
),
|
|
2411
|
-
diagnostics.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Checking configuration\u2026" }),
|
|
2412
|
-
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: [
|
|
2413
|
-
"Configuration checks could not run: ",
|
|
2414
|
-
diagnostics.message
|
|
2415
|
-
] }) })
|
|
2416
|
-
] }),
|
|
2417
|
-
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data }),
|
|
2418
|
-
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: state.envelope.data }),
|
|
2419
|
-
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
2472
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
2420
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: [
|
|
2421
2474
|
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2422
2475
|
state.revalidationError
|
|
2423
2476
|
] }) }),
|
|
2424
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
2425
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: state.envelope.notices }),
|
|
2426
2477
|
state.envelope.comparison && COMPARED_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2427
2478
|
"Changes are against ",
|
|
2428
2479
|
state.envelope.comparison.range.start,
|
|
2429
2480
|
" to ",
|
|
2430
2481
|
state.envelope.comparison.range.end,
|
|
2431
2482
|
", the equivalent window immediately before this one.",
|
|
2432
|
-
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." })
|
|
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 })
|
|
2433
2504
|
] }),
|
|
2434
2505
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2435
2506
|
"Figures cover ",
|
|
@@ -2780,6 +2851,7 @@ var index_default = visitorInsights;
|
|
|
2780
2851
|
formatMoney,
|
|
2781
2852
|
formatPercent,
|
|
2782
2853
|
isReportName,
|
|
2854
|
+
knownShortfall,
|
|
2783
2855
|
ok,
|
|
2784
2856
|
partial,
|
|
2785
2857
|
previousRange,
|