@liiift-studio/sanity-visitor-insights 0.24.0 → 0.25.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 +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +113 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +113 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/CrossSourceTimeline.tsx +100 -18
- package/src/studio/VisitorInsightsTool.tsx +42 -6
- package/src/studio/panels.test.tsx +74 -1
- package/src/studio/panels.tsx +62 -0
package/dist/index.d.mts
CHANGED
|
@@ -32,7 +32,6 @@ interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolP
|
|
|
32
32
|
options?: Partial<VisitorInsightsToolProps>;
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
/** The tool itself. */
|
|
36
35
|
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
37
36
|
|
|
38
37
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolP
|
|
|
32
32
|
options?: Partial<VisitorInsightsToolProps>;
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
/** The tool itself. */
|
|
36
35
|
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
37
36
|
|
|
38
37
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1060,6 +1060,7 @@ var GUTTER = 52;
|
|
|
1060
1060
|
var RIGHT_PAD = 12;
|
|
1061
1061
|
var TOP_PAD = 22;
|
|
1062
1062
|
var AXIS_TYPE = 11;
|
|
1063
|
+
var LABEL_STRIP = 16;
|
|
1063
1064
|
var MIN_BRUSH_DAYS = 3;
|
|
1064
1065
|
function formatValue(value, unit, currency) {
|
|
1065
1066
|
if (unit === "money") return formatMoney(value, currency ?? null);
|
|
@@ -1081,6 +1082,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1081
1082
|
const [pinned, setPinned] = (0, import_react3.useState)(false);
|
|
1082
1083
|
const [brushAnchor, setBrushAnchor] = (0, import_react3.useState)(null);
|
|
1083
1084
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1085
|
+
const [tooShort, setTooShort] = (0, import_react3.useState)(false);
|
|
1084
1086
|
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
1085
1087
|
const frameRef = (0, import_react3.useRef)(null);
|
|
1086
1088
|
const [frameNode, setFrameNode] = (0, import_react3.useState)(null);
|
|
@@ -1127,7 +1129,12 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1127
1129
|
);
|
|
1128
1130
|
const commit = (0, import_react3.useCallback)(
|
|
1129
1131
|
(from, to) => {
|
|
1130
|
-
if (!onBrush
|
|
1132
|
+
if (!onBrush) return;
|
|
1133
|
+
if (to - from < MIN_BRUSH_DAYS - 1) {
|
|
1134
|
+
setTooShort(true);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
setTooShort(false);
|
|
1131
1138
|
const start = dates[from];
|
|
1132
1139
|
const end = dates[to];
|
|
1133
1140
|
if (start && end) onBrush(start, end);
|
|
@@ -1145,6 +1152,14 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1145
1152
|
);
|
|
1146
1153
|
if (dates.length < 3 || series.length === 0 || !x) return null;
|
|
1147
1154
|
const hoveredDate = hoverIndex !== null ? dates[hoverIndex] : null;
|
|
1155
|
+
const tickIndexes = (0, import_react3.useMemo)(() => {
|
|
1156
|
+
if (dates.length === 0) return [];
|
|
1157
|
+
if (dates.length === 1) return [0];
|
|
1158
|
+
const affordable = Math.max(2, Math.min(8, Math.floor(plotWidth / 90)));
|
|
1159
|
+
const step = (dates.length - 1) / (affordable - 1);
|
|
1160
|
+
const indexes = Array.from({ length: affordable }, (_, i) => Math.round(i * step));
|
|
1161
|
+
return [...new Set(indexes)];
|
|
1162
|
+
}, [dates.length, plotWidth]);
|
|
1148
1163
|
const revealed = pinned || hoverIndex !== null;
|
|
1149
1164
|
const shapeOf = (row) => {
|
|
1150
1165
|
const real = row.points.filter((p) => p.value !== null);
|
|
@@ -1164,14 +1179,22 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1164
1179
|
if (!row.shortfall) return [];
|
|
1165
1180
|
const seen = new Map(row.shortfall.points.map((p) => [p.date, p.value]));
|
|
1166
1181
|
let gap = 0;
|
|
1182
|
+
let excess = 0;
|
|
1167
1183
|
let worst = null;
|
|
1184
|
+
const busiest = row.points.reduce((best, p) => Math.max(best, p.value ?? 0), 0);
|
|
1185
|
+
const floor = Math.max(1, busiest * 0.1);
|
|
1168
1186
|
for (const point of row.points) {
|
|
1169
1187
|
if (point.value === null) continue;
|
|
1170
1188
|
const saw = seen.get(point.date);
|
|
1171
1189
|
if (saw == null) continue;
|
|
1172
|
-
|
|
1190
|
+
const difference = point.value - saw;
|
|
1191
|
+
if (difference >= 0) gap += difference;
|
|
1192
|
+
else excess += -difference;
|
|
1173
1193
|
const ratio = point.value > 0 ? 1 - saw / point.value : 0;
|
|
1174
|
-
if (!worst || ratio > worst.ratio) worst = { date: point.date, ratio };
|
|
1194
|
+
if (point.value >= floor && (!worst || ratio > worst.ratio)) worst = { date: point.date, ratio };
|
|
1195
|
+
}
|
|
1196
|
+
if (excess > gap) {
|
|
1197
|
+
return [`${row.shortfall.source} counted ${formatCount(Math.round(excess - gap))} MORE ${row.label.toLowerCase()} than ${row.source} over this period, which usually means a tag firing twice rather than extra traffic.`];
|
|
1175
1198
|
}
|
|
1176
1199
|
if (gap <= 0 || !worst) return [];
|
|
1177
1200
|
return [`${row.shortfall.source} missed ${formatCount(Math.round(gap))} ${row.label.toLowerCase()} over this period; the gap was widest on ${tickLabel(/* @__PURE__ */ new Date(`${worst.date}T00:00:00Z`))} at ${formatPercent(worst.ratio, 0)}.`];
|
|
@@ -1183,14 +1206,15 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1183
1206
|
{
|
|
1184
1207
|
ref: attachFrame,
|
|
1185
1208
|
viewBox: `0 0 ${measured} ${height}`,
|
|
1186
|
-
|
|
1187
|
-
|
|
1209
|
+
onPointerMove: onMove,
|
|
1210
|
+
onPointerLeave: () => {
|
|
1188
1211
|
setHoverIndex(null);
|
|
1189
1212
|
setBrushAnchor(null);
|
|
1190
1213
|
setBrushed(null);
|
|
1191
1214
|
},
|
|
1192
1215
|
onPointerDown: (event) => {
|
|
1193
1216
|
if (!onBrush) return;
|
|
1217
|
+
setTooShort(false);
|
|
1194
1218
|
const index = indexAt(event.clientX);
|
|
1195
1219
|
if (index === null) return;
|
|
1196
1220
|
event.preventDefault();
|
|
@@ -1246,9 +1270,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1246
1270
|
series.map((row, rowIndex) => {
|
|
1247
1271
|
const top = TOP_PAD + rowIndex * ROW_HEIGHT;
|
|
1248
1272
|
const bottom = top + ROW_HEIGHT - 18;
|
|
1273
|
+
const plotTop = top + LABEL_STRIP;
|
|
1249
1274
|
const values = row.points.map((p) => p.value).filter((v) => v !== null);
|
|
1250
1275
|
const peak = (0, import_d3_array.max)(values) ?? 0;
|
|
1251
|
-
const y = (0, import_d3_scale.scaleLinear)().domain([0, peak || 1]).nice().range([bottom,
|
|
1276
|
+
const y = (0, import_d3_scale.scaleLinear)().domain([0, peak || 1]).nice().range([bottom, plotTop]);
|
|
1252
1277
|
const at = (p) => x(/* @__PURE__ */ new Date(`${p.date}T00:00:00Z`));
|
|
1253
1278
|
const defined = (p) => p.value !== null;
|
|
1254
1279
|
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveMonotoneX);
|
|
@@ -1264,13 +1289,13 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1264
1289
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints.filter((p) => p.value !== null)) ?? "" : "";
|
|
1265
1290
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
1266
1291
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: bottom, y2: bottom, stroke: "currentColor", strokeWidth: 1, opacity: 0.45 }),
|
|
1267
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER + 4, y: top +
|
|
1268
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("text", { x: GUTTER + plotWidth, y: top +
|
|
1292
|
+
/* @__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 }),
|
|
1293
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("text", { x: GUTTER + plotWidth, y: top + 11, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.72, children: [
|
|
1269
1294
|
row.source,
|
|
1270
1295
|
row.complete ? "" : " \xB7 partial"
|
|
1271
1296
|
] }),
|
|
1272
1297
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: y(y.domain()[1]) + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(y.domain()[1], row.unit, currency) }),
|
|
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:
|
|
1298
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: bottom + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(0, row.unit, currency) }),
|
|
1274
1299
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1275
1300
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
1276
1301
|
zeroes.map((p) => {
|
|
@@ -1375,7 +1400,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1375
1400
|
opacity: 0.8
|
|
1376
1401
|
}
|
|
1377
1402
|
),
|
|
1378
|
-
|
|
1403
|
+
tickIndexes.map((index, position) => {
|
|
1379
1404
|
const date = dates[index];
|
|
1380
1405
|
if (!date) return null;
|
|
1381
1406
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -1383,10 +1408,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1383
1408
|
{
|
|
1384
1409
|
x: x(/* @__PURE__ */ new Date(`${date}T00:00:00Z`)),
|
|
1385
1410
|
y: height - 6,
|
|
1386
|
-
textAnchor: position === 0 ? "start" : position ===
|
|
1411
|
+
textAnchor: position === 0 ? "start" : position === tickIndexes.length - 1 ? "end" : "middle",
|
|
1387
1412
|
fontSize: AXIS_TYPE,
|
|
1388
1413
|
fill: "currentColor",
|
|
1389
|
-
opacity: 0.
|
|
1414
|
+
opacity: 0.72,
|
|
1390
1415
|
children: tickLabel(/* @__PURE__ */ new Date(`${date}T00:00:00Z`))
|
|
1391
1416
|
},
|
|
1392
1417
|
date
|
|
@@ -1448,7 +1473,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1448
1473
|
)),
|
|
1449
1474
|
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "Shaded: what your analytics did not see." }),
|
|
1450
1475
|
markers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "Vertical rules mark campaign sends." }),
|
|
1451
|
-
onBrush &&
|
|
1476
|
+
onBrush && // The refusal replaces the instruction rather than sitting beside it, and it is a
|
|
1477
|
+
// live region: the reader has just dragged and is looking at the chart, not at the
|
|
1478
|
+
// small print under it.
|
|
1479
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: !tooShort, "aria-live": "polite", children: tooShort ? `That span was too short to apply \u2014 the chart needs at least ${MIN_BRUSH_DAYS} days. Drag a little wider.` : "Drag across at least three days \u2014 or hold shift and use the arrow keys, then Enter \u2014 to narrow every panel to that span." })
|
|
1452
1480
|
] })
|
|
1453
1481
|
] });
|
|
1454
1482
|
}
|
|
@@ -1520,6 +1548,14 @@ var cardGrid = {
|
|
|
1520
1548
|
gridTemplateColumns: "repeat(auto-fit, minmax(min(14rem, 100%), 1fr))",
|
|
1521
1549
|
gap: 16
|
|
1522
1550
|
};
|
|
1551
|
+
function gapOf(day) {
|
|
1552
|
+
if (day.vercelPageviews === null || day.ga4Pageviews === null) return null;
|
|
1553
|
+
return day.vercelPageviews - day.ga4Pageviews;
|
|
1554
|
+
}
|
|
1555
|
+
function coverageOf(day) {
|
|
1556
|
+
if (day.vercelPageviews === null || day.ga4Pageviews === null || day.vercelPageviews <= 0) return null;
|
|
1557
|
+
return day.ga4Pageviews / day.vercelPageviews;
|
|
1558
|
+
}
|
|
1523
1559
|
function OverviewPanel({ data, previous, onBrush }) {
|
|
1524
1560
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
1525
1561
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Verdict, { data, previous }),
|
|
@@ -1644,6 +1680,44 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1644
1680
|
sortValue: (d) => d.ga4Pageviews,
|
|
1645
1681
|
render: (d) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: d.ga4Pageviews === null ? "\u2014" : formatCount(d.ga4Pageviews) })
|
|
1646
1682
|
},
|
|
1683
|
+
{
|
|
1684
|
+
/*
|
|
1685
|
+
* The disagreement, as a number you can sort on.
|
|
1686
|
+
*
|
|
1687
|
+
* This table held `Pageviews` and `Seen by GA4` as separate columns and
|
|
1688
|
+
* nothing joining them, so the one quantity this whole tool exists to
|
|
1689
|
+
* surface was the one column missing from the only table that could hold
|
|
1690
|
+
* it. "Which day did GA4 lose most" was answerable by eye, or by the
|
|
1691
|
+
* single worst-day sentence under the chart, and by nothing else — not
|
|
1692
|
+
* sortable, not filterable, not in the CSV.
|
|
1693
|
+
*/
|
|
1694
|
+
key: "missed",
|
|
1695
|
+
label: "Missed by GA4",
|
|
1696
|
+
numeric: true,
|
|
1697
|
+
sortValue: (d) => gapOf(d),
|
|
1698
|
+
exportValue: (d) => gapOf(d),
|
|
1699
|
+
render: (d) => {
|
|
1700
|
+
const gap = gapOf(d);
|
|
1701
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: gap === null ? "\u2014" : formatCount(gap) });
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
/*
|
|
1706
|
+
* The same disagreement as a proportion, because the two rank days
|
|
1707
|
+
* differently and both questions are real: the absolute column finds the
|
|
1708
|
+
* day that cost the most, this one finds the day the instrument worked
|
|
1709
|
+
* worst. Sorting on it is how a reader dates a collapse.
|
|
1710
|
+
*/
|
|
1711
|
+
key: "coverage",
|
|
1712
|
+
label: "GA4 coverage",
|
|
1713
|
+
numeric: true,
|
|
1714
|
+
sortValue: (d) => coverageOf(d),
|
|
1715
|
+
exportValue: (d) => coverageOf(d),
|
|
1716
|
+
render: (d) => {
|
|
1717
|
+
const coverage = coverageOf(d);
|
|
1718
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: coverage === null ? "\u2014" : formatPercent(coverage, 0) });
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1647
1721
|
{
|
|
1648
1722
|
key: "orders",
|
|
1649
1723
|
label: "Orders",
|
|
@@ -2522,11 +2596,26 @@ var COMPARED_TABS = ["overview", "acquisition"];
|
|
|
2522
2596
|
var PanelBoundary = class extends import_react4.default.Component {
|
|
2523
2597
|
constructor(props) {
|
|
2524
2598
|
super(props);
|
|
2525
|
-
this.state = { error: null };
|
|
2599
|
+
this.state = { error: null, shownFor: props.resetKey };
|
|
2526
2600
|
}
|
|
2527
2601
|
static getDerivedStateFromError(error) {
|
|
2528
2602
|
return { error };
|
|
2529
2603
|
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Clear a caught error when the tab changes, WITHOUT remounting the subtree.
|
|
2606
|
+
*
|
|
2607
|
+
* This boundary used to be keyed on the active tab, which cleared the error by throwing the
|
|
2608
|
+
* whole subtree away — and with it every table's sort, filter and exclusions, and `useReport`'s
|
|
2609
|
+
* state, which resets to `idle` and refetches. So the natural exploratory loop — sort
|
|
2610
|
+
* Acquisition by engagement, check Journey, come back — always cost the reader their work, on a
|
|
2611
|
+
* component that invests real care in preserving exactly that state across a RANGE change.
|
|
2612
|
+
*
|
|
2613
|
+
* A reset key does the one thing the mount key was there for and nothing else.
|
|
2614
|
+
*/
|
|
2615
|
+
static getDerivedStateFromProps(props, state) {
|
|
2616
|
+
if (props.resetKey !== state.shownFor) return { error: null, shownFor: props.resetKey };
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2530
2619
|
componentDidCatch(error) {
|
|
2531
2620
|
console.error("Visitor insights: panel render failed:", error.message);
|
|
2532
2621
|
}
|
|
@@ -2546,6 +2635,7 @@ function VisitorInsightsTool(props) {
|
|
|
2546
2635
|
const [range, setRange] = (0, import_react4.useState)("week");
|
|
2547
2636
|
const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react4.useState)("week");
|
|
2548
2637
|
const [custom, setCustom] = (0, import_react4.useState)(() => ({ start: isoDaysAgo(30), end: isoDaysAgo(0) }));
|
|
2638
|
+
const atPresent = custom.end >= isoDaysAgo(0);
|
|
2549
2639
|
const [activePanel, setActivePanel] = (0, import_react4.useState)("overview");
|
|
2550
2640
|
const active = PANELS.find((p) => p.id === activePanel) ?? PANELS[0];
|
|
2551
2641
|
return (
|
|
@@ -2588,9 +2678,15 @@ function VisitorInsightsTool(props) {
|
|
|
2588
2678
|
type: "button",
|
|
2589
2679
|
style: inlineClear,
|
|
2590
2680
|
"aria-label": "Shift the window forward by its own length",
|
|
2681
|
+
disabled: atPresent,
|
|
2682
|
+
title: atPresent ? "This window already ends today" : void 0,
|
|
2591
2683
|
onClick: () => {
|
|
2592
2684
|
const span = daysBetween(custom.start, custom.end);
|
|
2593
|
-
|
|
2685
|
+
const today = isoDaysAgo(0);
|
|
2686
|
+
const end = shiftDays(custom.end, span);
|
|
2687
|
+
const allowed = end > today ? daysBetween(custom.end, today) : span;
|
|
2688
|
+
if (allowed <= 0) return;
|
|
2689
|
+
setCustom({ start: shiftDays(custom.start, allowed), end: shiftDays(custom.end, allowed) });
|
|
2594
2690
|
},
|
|
2595
2691
|
children: "later \u2192"
|
|
2596
2692
|
}
|
|
@@ -2610,7 +2706,7 @@ function VisitorInsightsTool(props) {
|
|
|
2610
2706
|
style: { position: "relative" },
|
|
2611
2707
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2612
2708
|
active && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: active.blurb }),
|
|
2613
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2709
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanelBoundary, { resetKey: activePanel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
2614
2710
|
ReportPanel,
|
|
2615
2711
|
{
|
|
2616
2712
|
tabId: active?.id ?? "overview",
|
|
@@ -2624,7 +2720,7 @@ function VisitorInsightsTool(props) {
|
|
|
2624
2720
|
setRange("custom");
|
|
2625
2721
|
}
|
|
2626
2722
|
}
|
|
2627
|
-
) }
|
|
2723
|
+
) })
|
|
2628
2724
|
] })
|
|
2629
2725
|
}
|
|
2630
2726
|
)
|