@liiift-studio/sanity-visitor-insights 0.25.0 → 0.26.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 +36 -32
- package/dist/index.d.ts +36 -32
- package/dist/index.js +116 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +116 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/CrossSourceTimeline.tsx +80 -20
- package/src/studio/Figure.tsx +7 -1
- package/src/studio/VisitorInsightsTool.tsx +132 -86
- package/src/studio/panels.test.tsx +167 -39
- package/src/studio/panels.tsx +11 -2
package/dist/index.d.mts
CHANGED
|
@@ -3,37 +3,6 @@ import React from 'react';
|
|
|
3
3
|
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-BUZuS1c-.mjs';
|
|
4
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
|
-
/**
|
|
7
|
-
* The Visitor Insights Studio tool.
|
|
8
|
-
*
|
|
9
|
-
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
10
|
-
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
11
|
-
* inconsistent between them.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** Options supplied by the plugin config, carried on the Sanity tool definition. */
|
|
15
|
-
interface VisitorInsightsToolProps {
|
|
16
|
-
apiBaseUrl: string;
|
|
17
|
-
siteLabel: string;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* What Sanity actually hands a tool component.
|
|
21
|
-
*
|
|
22
|
-
* Sanity does NOT spread a tool's `options` onto its component's props — it passes the whole tool
|
|
23
|
-
* definition as `tool`, with the options nested inside. Destructuring `apiBaseUrl` straight off
|
|
24
|
-
* props therefore yields undefined, and the first thing useReport does with it is call .replace(),
|
|
25
|
-
* so every panel failed with "Cannot read properties of undefined (reading 'replace')".
|
|
26
|
-
*
|
|
27
|
-
* Both shapes are accepted: the nested one because that is what the Studio passes, and the flat one
|
|
28
|
-
* because the component is exported for direct use and is mounted that way in tests.
|
|
29
|
-
*/
|
|
30
|
-
interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolProps> {
|
|
31
|
-
tool?: {
|
|
32
|
-
options?: Partial<VisitorInsightsToolProps>;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
36
|
-
|
|
37
6
|
/**
|
|
38
7
|
* Data hook for the Studio panels.
|
|
39
8
|
*
|
|
@@ -100,6 +69,37 @@ declare function useReport<T>({ apiBaseUrl, report, range, custom, enabled }: Us
|
|
|
100
69
|
reload: () => void;
|
|
101
70
|
};
|
|
102
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The Visitor Insights Studio tool.
|
|
74
|
+
*
|
|
75
|
+
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
76
|
+
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
77
|
+
* inconsistent between them.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** Options supplied by the plugin config, carried on the Sanity tool definition. */
|
|
81
|
+
interface VisitorInsightsToolProps {
|
|
82
|
+
apiBaseUrl: string;
|
|
83
|
+
siteLabel: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What Sanity actually hands a tool component.
|
|
87
|
+
*
|
|
88
|
+
* Sanity does NOT spread a tool's `options` onto its component's props — it passes the whole tool
|
|
89
|
+
* definition as `tool`, with the options nested inside. Destructuring `apiBaseUrl` straight off
|
|
90
|
+
* props therefore yields undefined, and the first thing useReport does with it is call .replace(),
|
|
91
|
+
* so every panel failed with "Cannot read properties of undefined (reading 'replace')".
|
|
92
|
+
*
|
|
93
|
+
* Both shapes are accepted: the nested one because that is what the Studio passes, and the flat one
|
|
94
|
+
* because the component is exported for direct use and is mounted that way in tests.
|
|
95
|
+
*/
|
|
96
|
+
interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolProps> {
|
|
97
|
+
tool?: {
|
|
98
|
+
options?: Partial<VisitorInsightsToolProps>;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
102
|
+
|
|
103
103
|
/**
|
|
104
104
|
* Shared renderers for metric values and comparison bars.
|
|
105
105
|
*
|
|
@@ -199,6 +199,10 @@ interface ChartDataProps<Row> {
|
|
|
199
199
|
columns: Array<SortColumn<Row>>;
|
|
200
200
|
rowKey: (row: Row) => string;
|
|
201
201
|
exportName?: string;
|
|
202
|
+
/** A line above the table, for anything a column header cannot carry. */
|
|
203
|
+
note?: string;
|
|
204
|
+
/** Which column to filter on, if the table should offer a filter box. */
|
|
205
|
+
filterOn?: (row: Row) => string;
|
|
202
206
|
}
|
|
203
207
|
/**
|
|
204
208
|
* The figures behind a chart, as a table, collapsed by default.
|
|
@@ -212,7 +216,7 @@ interface ChartDataProps<Row> {
|
|
|
212
216
|
* axis type at any size; and anyone who wants these numbers in a spreadsheet, since the table
|
|
213
217
|
* brings sorting and CSV copy with it. Collapsed, so it costs a sighted reader one line.
|
|
214
218
|
*/
|
|
215
|
-
declare function ChartData<Row>({ label, rows, columns, rowKey, exportName }: ChartDataProps<Row>): React.ReactElement | null;
|
|
219
|
+
declare function ChartData<Row>({ label, rows, columns, rowKey, exportName, note, filterOn }: ChartDataProps<Row>): React.ReactElement | null;
|
|
216
220
|
/** One bar of a proportion chart. */
|
|
217
221
|
interface ProportionBar {
|
|
218
222
|
key: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,37 +3,6 @@ import React from 'react';
|
|
|
3
3
|
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-BUZuS1c-.js';
|
|
4
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
|
-
/**
|
|
7
|
-
* The Visitor Insights Studio tool.
|
|
8
|
-
*
|
|
9
|
-
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
10
|
-
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
11
|
-
* inconsistent between them.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** Options supplied by the plugin config, carried on the Sanity tool definition. */
|
|
15
|
-
interface VisitorInsightsToolProps {
|
|
16
|
-
apiBaseUrl: string;
|
|
17
|
-
siteLabel: string;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* What Sanity actually hands a tool component.
|
|
21
|
-
*
|
|
22
|
-
* Sanity does NOT spread a tool's `options` onto its component's props — it passes the whole tool
|
|
23
|
-
* definition as `tool`, with the options nested inside. Destructuring `apiBaseUrl` straight off
|
|
24
|
-
* props therefore yields undefined, and the first thing useReport does with it is call .replace(),
|
|
25
|
-
* so every panel failed with "Cannot read properties of undefined (reading 'replace')".
|
|
26
|
-
*
|
|
27
|
-
* Both shapes are accepted: the nested one because that is what the Studio passes, and the flat one
|
|
28
|
-
* because the component is exported for direct use and is mounted that way in tests.
|
|
29
|
-
*/
|
|
30
|
-
interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolProps> {
|
|
31
|
-
tool?: {
|
|
32
|
-
options?: Partial<VisitorInsightsToolProps>;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
36
|
-
|
|
37
6
|
/**
|
|
38
7
|
* Data hook for the Studio panels.
|
|
39
8
|
*
|
|
@@ -100,6 +69,37 @@ declare function useReport<T>({ apiBaseUrl, report, range, custom, enabled }: Us
|
|
|
100
69
|
reload: () => void;
|
|
101
70
|
};
|
|
102
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The Visitor Insights Studio tool.
|
|
74
|
+
*
|
|
75
|
+
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
76
|
+
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
77
|
+
* inconsistent between them.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** Options supplied by the plugin config, carried on the Sanity tool definition. */
|
|
81
|
+
interface VisitorInsightsToolProps {
|
|
82
|
+
apiBaseUrl: string;
|
|
83
|
+
siteLabel: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What Sanity actually hands a tool component.
|
|
87
|
+
*
|
|
88
|
+
* Sanity does NOT spread a tool's `options` onto its component's props — it passes the whole tool
|
|
89
|
+
* definition as `tool`, with the options nested inside. Destructuring `apiBaseUrl` straight off
|
|
90
|
+
* props therefore yields undefined, and the first thing useReport does with it is call .replace(),
|
|
91
|
+
* so every panel failed with "Cannot read properties of undefined (reading 'replace')".
|
|
92
|
+
*
|
|
93
|
+
* Both shapes are accepted: the nested one because that is what the Studio passes, and the flat one
|
|
94
|
+
* because the component is exported for direct use and is mounted that way in tests.
|
|
95
|
+
*/
|
|
96
|
+
interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolProps> {
|
|
97
|
+
tool?: {
|
|
98
|
+
options?: Partial<VisitorInsightsToolProps>;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
102
|
+
|
|
103
103
|
/**
|
|
104
104
|
* Shared renderers for metric values and comparison bars.
|
|
105
105
|
*
|
|
@@ -199,6 +199,10 @@ interface ChartDataProps<Row> {
|
|
|
199
199
|
columns: Array<SortColumn<Row>>;
|
|
200
200
|
rowKey: (row: Row) => string;
|
|
201
201
|
exportName?: string;
|
|
202
|
+
/** A line above the table, for anything a column header cannot carry. */
|
|
203
|
+
note?: string;
|
|
204
|
+
/** Which column to filter on, if the table should offer a filter box. */
|
|
205
|
+
filterOn?: (row: Row) => string;
|
|
202
206
|
}
|
|
203
207
|
/**
|
|
204
208
|
* The figures behind a chart, as a table, collapsed by default.
|
|
@@ -212,7 +216,7 @@ interface ChartDataProps<Row> {
|
|
|
212
216
|
* axis type at any size; and anyone who wants these numbers in a spreadsheet, since the table
|
|
213
217
|
* brings sorting and CSV copy with it. Collapsed, so it costs a sighted reader one line.
|
|
214
218
|
*/
|
|
215
|
-
declare function ChartData<Row>({ label, rows, columns, rowKey, exportName }: ChartDataProps<Row>): React.ReactElement | null;
|
|
219
|
+
declare function ChartData<Row>({ label, rows, columns, rowKey, exportName, note, filterOn }: ChartDataProps<Row>): React.ReactElement | null;
|
|
216
220
|
/** One bar of a proportion chart. */
|
|
217
221
|
interface ProportionBar {
|
|
218
222
|
key: string;
|
package/dist/index.js
CHANGED
|
@@ -365,20 +365,24 @@ function ComparisonBar({ label, metric, max, outOf }) {
|
|
|
365
365
|
outOf && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: outOf })
|
|
366
366
|
] });
|
|
367
367
|
}
|
|
368
|
-
function ChartData({ label, rows, columns, rowKey, exportName }) {
|
|
368
|
+
function ChartData({ label, rows, columns, rowKey, exportName, note, filterOn }) {
|
|
369
369
|
if (rows.length === 0) return null;
|
|
370
370
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: disclosureBlock, children: [
|
|
371
371
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { style: disclosureSummary, children: label }),
|
|
372
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
372
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 10 }, children: [
|
|
373
|
+
note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, style: { marginBottom: 8, display: "block" }, children: note }),
|
|
374
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
375
|
+
SortableTable,
|
|
376
|
+
{
|
|
377
|
+
caption: label,
|
|
378
|
+
rows,
|
|
379
|
+
columns,
|
|
380
|
+
rowKey,
|
|
381
|
+
exportName,
|
|
382
|
+
filterOn
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
] })
|
|
382
386
|
] });
|
|
383
387
|
}
|
|
384
388
|
var disclosureBlock = { width: "100%" };
|
|
@@ -1054,7 +1058,7 @@ var import_d3_shape = require("d3-shape");
|
|
|
1054
1058
|
var import_d3_array = require("d3-array");
|
|
1055
1059
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1056
1060
|
var WIDTH = 760;
|
|
1057
|
-
var ROW_HEIGHT =
|
|
1061
|
+
var ROW_HEIGHT = 92;
|
|
1058
1062
|
var AXIS_HEIGHT = 26;
|
|
1059
1063
|
var GUTTER = 52;
|
|
1060
1064
|
var RIGHT_PAD = 12;
|
|
@@ -1131,7 +1135,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1131
1135
|
(from, to) => {
|
|
1132
1136
|
if (!onBrush) return;
|
|
1133
1137
|
if (to - from < MIN_BRUSH_DAYS - 1) {
|
|
1134
|
-
setTooShort(
|
|
1138
|
+
setTooShort(to > from);
|
|
1135
1139
|
return;
|
|
1136
1140
|
}
|
|
1137
1141
|
setTooShort(false);
|
|
@@ -1146,12 +1150,14 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1146
1150
|
const index = indexAt(event.clientX);
|
|
1147
1151
|
if (index === null) return;
|
|
1148
1152
|
setHoverIndex(index);
|
|
1149
|
-
if (brushAnchor !== null)
|
|
1153
|
+
if (brushAnchor !== null) {
|
|
1154
|
+
const from = Math.min(brushAnchor, index);
|
|
1155
|
+
const to = Math.max(brushAnchor, index);
|
|
1156
|
+
setBrushed((current) => current && current[0] === from && current[1] === to ? current : [from, to]);
|
|
1157
|
+
}
|
|
1150
1158
|
},
|
|
1151
1159
|
[indexAt, brushAnchor]
|
|
1152
1160
|
);
|
|
1153
|
-
if (dates.length < 3 || series.length === 0 || !x) return null;
|
|
1154
|
-
const hoveredDate = hoverIndex !== null ? dates[hoverIndex] : null;
|
|
1155
1161
|
const tickIndexes = (0, import_react3.useMemo)(() => {
|
|
1156
1162
|
if (dates.length === 0) return [];
|
|
1157
1163
|
if (dates.length === 1) return [0];
|
|
@@ -1160,6 +1166,8 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1160
1166
|
const indexes = Array.from({ length: affordable }, (_, i) => Math.round(i * step));
|
|
1161
1167
|
return [...new Set(indexes)];
|
|
1162
1168
|
}, [dates.length, plotWidth]);
|
|
1169
|
+
if (dates.length < 3 || series.length === 0 || !x) return null;
|
|
1170
|
+
const hoveredDate = hoverIndex !== null ? dates[hoverIndex] : null;
|
|
1163
1171
|
const revealed = pinned || hoverIndex !== null;
|
|
1164
1172
|
const shapeOf = (row) => {
|
|
1165
1173
|
const real = row.points.filter((p) => p.value !== null);
|
|
@@ -1193,11 +1201,14 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1193
1201
|
const ratio = point.value > 0 ? 1 - saw / point.value : 0;
|
|
1194
1202
|
if (point.value >= floor && (!worst || ratio > worst.ratio)) worst = { date: point.date, ratio };
|
|
1195
1203
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1204
|
+
const sentences = [];
|
|
1205
|
+
if (excess > 0) {
|
|
1206
|
+
sentences.push(`${row.shortfall.source} counted ${formatValue(Math.round(excess), row.unit, currency)} MORE ${row.label.toLowerCase()} than ${row.source} on some days, which usually means a tag firing twice rather than extra traffic.`);
|
|
1198
1207
|
}
|
|
1199
|
-
if (gap <= 0
|
|
1200
|
-
|
|
1208
|
+
if (gap <= 0) return sentences;
|
|
1209
|
+
const total = `${row.shortfall.source} missed ${formatValue(Math.round(gap), row.unit, currency)} ${row.label.toLowerCase()} over this period`;
|
|
1210
|
+
sentences.push(worst && worst.ratio > 0 ? `${total}; the gap was widest on ${tickLabel(/* @__PURE__ */ new Date(`${worst.date}T00:00:00Z`))} at ${formatPercent(worst.ratio, 0)}.` : `${total}.`);
|
|
1211
|
+
return sentences;
|
|
1201
1212
|
});
|
|
1202
1213
|
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.` : "");
|
|
1203
1214
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
@@ -1211,6 +1222,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1211
1222
|
setHoverIndex(null);
|
|
1212
1223
|
setBrushAnchor(null);
|
|
1213
1224
|
setBrushed(null);
|
|
1225
|
+
setTooShort(false);
|
|
1214
1226
|
},
|
|
1215
1227
|
onPointerDown: (event) => {
|
|
1216
1228
|
if (!onBrush) return;
|
|
@@ -1286,7 +1298,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1286
1298
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
1287
1299
|
const zeroes = row.mark === "events" ? row.points.filter((p) => p.value === 0) : [];
|
|
1288
1300
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1289
|
-
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints
|
|
1301
|
+
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints) ?? "" : "";
|
|
1290
1302
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
1291
1303
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: bottom, y2: bottom, stroke: "currentColor", strokeWidth: 1, opacity: 0.45 }),
|
|
1292
1304
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER + 4, y: top + 11, fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.85, fontWeight: 500, children: row.label }),
|
|
@@ -1305,11 +1317,11 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1305
1317
|
"rect",
|
|
1306
1318
|
{
|
|
1307
1319
|
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1308
|
-
y: bottom -
|
|
1320
|
+
y: bottom - 3,
|
|
1309
1321
|
width: stemWidth,
|
|
1310
|
-
height:
|
|
1322
|
+
height: 3,
|
|
1311
1323
|
fill: "currentColor",
|
|
1312
|
-
opacity: 0.
|
|
1324
|
+
opacity: 0.55
|
|
1313
1325
|
},
|
|
1314
1326
|
`zero-${p.date}`
|
|
1315
1327
|
);
|
|
@@ -1660,10 +1672,12 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1660
1672
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1661
1673
|
ChartData,
|
|
1662
1674
|
{
|
|
1675
|
+
note: "A negative miss means GA4 counted MORE than Vercel that day \u2014 usually a tag firing twice, not extra traffic.",
|
|
1663
1676
|
label: "Show these figures as a table",
|
|
1664
1677
|
rows: data.crossSource ?? [],
|
|
1665
1678
|
rowKey: (d) => d.date,
|
|
1666
1679
|
exportName: "timeline",
|
|
1680
|
+
filterOn: (d) => d.date,
|
|
1667
1681
|
columns: [
|
|
1668
1682
|
{ key: "date", label: "Date", sortValue: (d) => d.date, render: (d) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: d.date }) },
|
|
1669
1683
|
{
|
|
@@ -1695,7 +1709,6 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1695
1709
|
label: "Missed by GA4",
|
|
1696
1710
|
numeric: true,
|
|
1697
1711
|
sortValue: (d) => gapOf(d),
|
|
1698
|
-
exportValue: (d) => gapOf(d),
|
|
1699
1712
|
render: (d) => {
|
|
1700
1713
|
const gap = gapOf(d);
|
|
1701
1714
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: gap === null ? "\u2014" : formatCount(gap) });
|
|
@@ -1712,7 +1725,13 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1712
1725
|
label: "GA4 coverage",
|
|
1713
1726
|
numeric: true,
|
|
1714
1727
|
sortValue: (d) => coverageOf(d),
|
|
1715
|
-
|
|
1728
|
+
// The FORMATTED value, matching the cell. Passing the raw ratio through
|
|
1729
|
+
// shipped 0.06666666666666667 into a CSV column whose cell reads "7%" —
|
|
1730
|
+
// verbatim the case exportValue exists to prevent.
|
|
1731
|
+
exportValue: (d) => {
|
|
1732
|
+
const coverage = coverageOf(d);
|
|
1733
|
+
return coverage === null ? null : formatPercent(coverage, 0);
|
|
1734
|
+
},
|
|
1716
1735
|
render: (d) => {
|
|
1717
1736
|
const coverage = coverageOf(d);
|
|
1718
1737
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: coverage === null ? "\u2014" : formatPercent(coverage, 0) });
|
|
@@ -2507,6 +2526,60 @@ function CoverageRibbon({ ratio }) {
|
|
|
2507
2526
|
", not as what happened. Data health explains why."
|
|
2508
2527
|
] }) });
|
|
2509
2528
|
}
|
|
2529
|
+
function ReadyReport({
|
|
2530
|
+
envelope,
|
|
2531
|
+
tabId,
|
|
2532
|
+
apiBaseUrl,
|
|
2533
|
+
range,
|
|
2534
|
+
custom,
|
|
2535
|
+
revalidationError,
|
|
2536
|
+
diagnostics,
|
|
2537
|
+
onBrush
|
|
2538
|
+
}) {
|
|
2539
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2540
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: envelope.sources }),
|
|
2541
|
+
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: [
|
|
2542
|
+
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2543
|
+
revalidationError
|
|
2544
|
+
] }) }),
|
|
2545
|
+
envelope.comparison && COMPARED_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2546
|
+
"Changes are against ",
|
|
2547
|
+
envelope.comparison.range.start,
|
|
2548
|
+
" to ",
|
|
2549
|
+
envelope.comparison.range.end,
|
|
2550
|
+
", the equivalent window immediately before this one.",
|
|
2551
|
+
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." })
|
|
2552
|
+
] }),
|
|
2553
|
+
GA4_ONLY_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CoverageRibbon, { ratio: knownShortfall(apiBaseUrl, range, range === "custom" ? custom : void 0) }),
|
|
2554
|
+
tabId === "overview" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OverviewPanel, { data: envelope.data, previous: envelope.comparison?.data, onBrush }),
|
|
2555
|
+
tabId === "data-health" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2556
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2557
|
+
DataHealthPanel,
|
|
2558
|
+
{
|
|
2559
|
+
data: envelope.data,
|
|
2560
|
+
diagnostics: diagnostics.status === "ready" ? diagnostics.envelope.data : void 0
|
|
2561
|
+
}
|
|
2562
|
+
),
|
|
2563
|
+
diagnostics.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Checking configuration\u2026" }),
|
|
2564
|
+
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: [
|
|
2565
|
+
"Configuration checks could not run: ",
|
|
2566
|
+
diagnostics.message
|
|
2567
|
+
] }) })
|
|
2568
|
+
] }),
|
|
2569
|
+
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: envelope.data, previous: envelope.comparison?.data }),
|
|
2570
|
+
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: envelope.data }),
|
|
2571
|
+
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: envelope.data }),
|
|
2572
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: envelope.notices }),
|
|
2573
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2574
|
+
"Figures cover ",
|
|
2575
|
+
envelope.range.start,
|
|
2576
|
+
" to ",
|
|
2577
|
+
envelope.range.end,
|
|
2578
|
+
", in ",
|
|
2579
|
+
envelope.range.timezone
|
|
2580
|
+
] })
|
|
2581
|
+
] });
|
|
2582
|
+
}
|
|
2510
2583
|
function ReportPanel({
|
|
2511
2584
|
tabId,
|
|
2512
2585
|
report,
|
|
@@ -2542,49 +2615,19 @@ function ReportPanel({
|
|
|
2542
2615
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: state.disabled, children: state.message }),
|
|
2543
2616
|
!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" }) })
|
|
2544
2617
|
] }) }),
|
|
2545
|
-
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
state.
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
GA4_ONLY_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CoverageRibbon, { ratio: knownShortfall(apiBaseUrl, range, range === "custom" ? custom : void 0) }),
|
|
2559
|
-
tabId === "overview" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OverviewPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data, onBrush }),
|
|
2560
|
-
tabId === "data-health" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2561
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2562
|
-
DataHealthPanel,
|
|
2563
|
-
{
|
|
2564
|
-
data: state.envelope.data,
|
|
2565
|
-
diagnostics: diagnostics.status === "ready" ? diagnostics.envelope.data : void 0
|
|
2566
|
-
}
|
|
2567
|
-
),
|
|
2568
|
-
diagnostics.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Checking configuration\u2026" }),
|
|
2569
|
-
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: [
|
|
2570
|
-
"Configuration checks could not run: ",
|
|
2571
|
-
diagnostics.message
|
|
2572
|
-
] }) })
|
|
2573
|
-
] }),
|
|
2574
|
-
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data }),
|
|
2575
|
-
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: state.envelope.data }),
|
|
2576
|
-
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
2577
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: state.envelope.notices })
|
|
2578
|
-
] }),
|
|
2579
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2580
|
-
"Figures cover ",
|
|
2581
|
-
state.envelope.range.start,
|
|
2582
|
-
" to ",
|
|
2583
|
-
state.envelope.range.end,
|
|
2584
|
-
", in ",
|
|
2585
|
-
state.envelope.range.timezone
|
|
2586
|
-
] })
|
|
2587
|
-
] })
|
|
2618
|
+
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2619
|
+
ReadyReport,
|
|
2620
|
+
{
|
|
2621
|
+
envelope: state.envelope,
|
|
2622
|
+
tabId,
|
|
2623
|
+
apiBaseUrl,
|
|
2624
|
+
range,
|
|
2625
|
+
custom,
|
|
2626
|
+
revalidationError: state.revalidationError ?? null,
|
|
2627
|
+
diagnostics,
|
|
2628
|
+
onBrush
|
|
2629
|
+
}
|
|
2630
|
+
)
|
|
2588
2631
|
] });
|
|
2589
2632
|
}
|
|
2590
2633
|
function isoDaysAgo(days) {
|
|
@@ -2676,15 +2719,15 @@ function VisitorInsightsTool(props) {
|
|
|
2676
2719
|
"button",
|
|
2677
2720
|
{
|
|
2678
2721
|
type: "button",
|
|
2679
|
-
style: inlineClear,
|
|
2680
2722
|
"aria-label": "Shift the window forward by its own length",
|
|
2681
2723
|
disabled: atPresent,
|
|
2724
|
+
style: atPresent ? { ...inlineClear, opacity: 0.35, cursor: "not-allowed" } : inlineClear,
|
|
2682
2725
|
title: atPresent ? "This window already ends today" : void 0,
|
|
2683
2726
|
onClick: () => {
|
|
2684
2727
|
const span = daysBetween(custom.start, custom.end);
|
|
2685
2728
|
const today = isoDaysAgo(0);
|
|
2686
|
-
const
|
|
2687
|
-
const allowed =
|
|
2729
|
+
const room = daysBetween(custom.end, today) - 1;
|
|
2730
|
+
const allowed = Math.min(span, room);
|
|
2688
2731
|
if (allowed <= 0) return;
|
|
2689
2732
|
setCustom({ start: shiftDays(custom.start, allowed), end: shiftDays(custom.end, allowed) });
|
|
2690
2733
|
},
|
|
@@ -2706,7 +2749,7 @@ function VisitorInsightsTool(props) {
|
|
|
2706
2749
|
style: { position: "relative" },
|
|
2707
2750
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2708
2751
|
active && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: active.blurb }),
|
|
2709
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanelBoundary, { resetKey: activePanel
|
|
2752
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanelBoundary, { resetKey: `${activePanel}|${range}|${custom.start}|${custom.end}`, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2710
2753
|
ReportPanel,
|
|
2711
2754
|
{
|
|
2712
2755
|
tabId: active?.id ?? "overview",
|