@liiift-studio/sanity-visitor-insights 0.48.0 → 0.50.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 +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +210 -92
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/Figure.tsx +227 -6
- package/src/studio/panels.test.tsx +164 -2
- package/src/studio/panels.tsx +66 -36
package/dist/index.d.mts
CHANGED
|
@@ -363,6 +363,13 @@ interface SortColumn<Row> {
|
|
|
363
363
|
* currency, and a column with a fallback displays something `sortValue` returns null for.
|
|
364
364
|
*/
|
|
365
365
|
exportValue?: (row: Row) => number | string | null;
|
|
366
|
+
/**
|
|
367
|
+
* Keep this column even when every value in it is blank or zero.
|
|
368
|
+
*
|
|
369
|
+
* For the column that identifies the row — a table whose first column collapsed would be a list
|
|
370
|
+
* of numbers belonging to nothing — and for any column whose emptiness is itself the finding.
|
|
371
|
+
*/
|
|
372
|
+
alwaysShow?: boolean;
|
|
366
373
|
render: (row: Row) => React.ReactNode;
|
|
367
374
|
}
|
|
368
375
|
/** Props for SortableTable. */
|
package/dist/index.d.ts
CHANGED
|
@@ -363,6 +363,13 @@ interface SortColumn<Row> {
|
|
|
363
363
|
* currency, and a column with a fallback displays something `sortValue` returns null for.
|
|
364
364
|
*/
|
|
365
365
|
exportValue?: (row: Row) => number | string | null;
|
|
366
|
+
/**
|
|
367
|
+
* Keep this column even when every value in it is blank or zero.
|
|
368
|
+
*
|
|
369
|
+
* For the column that identifies the row — a table whose first column collapsed would be a list
|
|
370
|
+
* of numbers belonging to nothing — and for any column whose emptiness is itself the finding.
|
|
371
|
+
*/
|
|
372
|
+
alwaysShow?: boolean;
|
|
366
373
|
render: (row: Row) => React.ReactNode;
|
|
367
374
|
}
|
|
368
375
|
/** Props for SortableTable. */
|
package/dist/index.js
CHANGED
|
@@ -648,6 +648,52 @@ function NoticeList({ notices }) {
|
|
|
648
648
|
)
|
|
649
649
|
] });
|
|
650
650
|
}
|
|
651
|
+
function columnIsEmpty(column, rows) {
|
|
652
|
+
if (column.alwaysShow) return false;
|
|
653
|
+
if (rows.length === 0) return false;
|
|
654
|
+
return rows.every((row) => {
|
|
655
|
+
const value = column.sortValue(row);
|
|
656
|
+
return value === 0 || value === "";
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function Section({
|
|
660
|
+
title,
|
|
661
|
+
subtitle,
|
|
662
|
+
tone = "primary",
|
|
663
|
+
collapsible = false,
|
|
664
|
+
defaultOpen = true,
|
|
665
|
+
children
|
|
666
|
+
}) {
|
|
667
|
+
const [open, setOpen] = import_react2.default.useState(defaultOpen);
|
|
668
|
+
const bodyId = import_react2.default.useId();
|
|
669
|
+
const shown = !collapsible || open;
|
|
670
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
671
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: sectionHeader, children: [
|
|
672
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 1, children: [
|
|
673
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Heading, { size: tone === "primary" ? 1 : 0, style: tone === "primary" ? primaryTitle : secondaryTitle, children: title }),
|
|
674
|
+
subtitle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: subtitle })
|
|
675
|
+
] }),
|
|
676
|
+
collapsible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
677
|
+
"button",
|
|
678
|
+
{
|
|
679
|
+
type: "button",
|
|
680
|
+
style: sectionToggle,
|
|
681
|
+
"aria-expanded": open,
|
|
682
|
+
"aria-controls": bodyId,
|
|
683
|
+
onClick: () => setOpen((was) => !was),
|
|
684
|
+
children: open ? "Hide" : "Show"
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
] }),
|
|
688
|
+
shown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: bodyId, children })
|
|
689
|
+
] }) });
|
|
690
|
+
}
|
|
691
|
+
function SectionTitle({
|
|
692
|
+
title,
|
|
693
|
+
tone = "primary"
|
|
694
|
+
}) {
|
|
695
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sectionHeader, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Heading, { size: tone === "primary" ? 1 : 0, style: tone === "primary" ? primaryTitle : secondaryTitle, children: title }) });
|
|
696
|
+
}
|
|
651
697
|
function SortableTable({
|
|
652
698
|
caption,
|
|
653
699
|
columns,
|
|
@@ -666,6 +712,7 @@ function SortableTable({
|
|
|
666
712
|
const [query, setQuery] = import_react2.default.useState("");
|
|
667
713
|
const [excluded, setExcluded] = import_react2.default.useState(() => /* @__PURE__ */ new Set());
|
|
668
714
|
const [copied, setCopied] = import_react2.default.useState("idle");
|
|
715
|
+
const [showEmpty, setShowEmpty] = import_react2.default.useState(false);
|
|
669
716
|
const copyTimer = import_react2.default.useRef(null);
|
|
670
717
|
import_react2.default.useEffect(() => () => {
|
|
671
718
|
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
|
|
@@ -679,6 +726,13 @@ function SortableTable({
|
|
|
679
726
|
return filterOn(row).toLowerCase().includes(needle);
|
|
680
727
|
});
|
|
681
728
|
}, [rows, query, excluded, filterOn, rowKey]);
|
|
729
|
+
const emptyColumns = import_react2.default.useMemo(
|
|
730
|
+
() => columns.filter(
|
|
731
|
+
(column, index) => index > 0 && column.key !== sort?.key && columnIsEmpty(column, visible)
|
|
732
|
+
),
|
|
733
|
+
[columns, visible, sort?.key]
|
|
734
|
+
);
|
|
735
|
+
const shown = showEmpty || emptyColumns.length === 0 ? columns : columns.filter((column) => !emptyColumns.includes(column));
|
|
682
736
|
const ordered = import_react2.default.useMemo(() => {
|
|
683
737
|
const rows2 = visible;
|
|
684
738
|
if (!active || !sort) return rows2;
|
|
@@ -701,8 +755,8 @@ function SortableTable({
|
|
|
701
755
|
});
|
|
702
756
|
};
|
|
703
757
|
const copyCsv = async () => {
|
|
704
|
-
const header =
|
|
705
|
-
const lines = [header, ...ordered.map((row) =>
|
|
758
|
+
const header = shown.map((column) => column.label);
|
|
759
|
+
const lines = [header, ...ordered.map((row) => shown.map((column) => {
|
|
706
760
|
const value = column.exportValue ? column.exportValue(row) : column.sortValue(row);
|
|
707
761
|
return value === null || value === void 0 ? "" : String(value);
|
|
708
762
|
}))];
|
|
@@ -722,7 +776,7 @@ function SortableTable({
|
|
|
722
776
|
const hiddenCount = rows.length - visible.length - excludedInWindow;
|
|
723
777
|
const excludedCount = excluded.size;
|
|
724
778
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
725
|
-
(filterOn || exportName) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: tableControls, children: [
|
|
779
|
+
(filterOn || exportName || emptyColumns.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: tableControls, children: [
|
|
726
780
|
filterOn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
727
781
|
"input",
|
|
728
782
|
{
|
|
@@ -750,11 +804,21 @@ function SortableTable({
|
|
|
750
804
|
] })
|
|
751
805
|
] }),
|
|
752
806
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
807
|
+
emptyColumns.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
808
|
+
"button",
|
|
809
|
+
{
|
|
810
|
+
type: "button",
|
|
811
|
+
style: tableControlButton,
|
|
812
|
+
"aria-pressed": showEmpty,
|
|
813
|
+
onClick: () => setShowEmpty((open) => !open),
|
|
814
|
+
children: showEmpty ? `Hide ${emptyColumns.length} empty column${emptyColumns.length === 1 ? "" : "s"}` : `Show ${emptyColumns.length} empty column${emptyColumns.length === 1 ? "" : "s"}`
|
|
815
|
+
}
|
|
816
|
+
),
|
|
753
817
|
exportName && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: tableControlButton, onClick: () => void copyCsv(), children: copied === "done" ? "Copied" : copied === "failed" ? "Could not copy" : "Copy as CSV" })
|
|
754
818
|
] }),
|
|
755
819
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Card, { radius: 2, tone: "transparent", border: true, style: tableWrapper, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: tableBase, children: [
|
|
756
820
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("caption", { style: visuallyHidden, children: caption }),
|
|
757
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children:
|
|
821
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: shown.map((column) => {
|
|
758
822
|
const isActive = sort?.key === column.key;
|
|
759
823
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
760
824
|
"th",
|
|
@@ -773,7 +837,7 @@ function SortableTable({
|
|
|
773
837
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tbody", { children: [
|
|
774
838
|
ordered.map((row) => {
|
|
775
839
|
const key = rowKey(row);
|
|
776
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children:
|
|
840
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: shown.map((column, index) => {
|
|
777
841
|
const content = column.render(row);
|
|
778
842
|
return index === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "row", style: bodyCell, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: firstCell, children: [
|
|
779
843
|
content,
|
|
@@ -813,6 +877,40 @@ var tableControls = {
|
|
|
813
877
|
gap: 10,
|
|
814
878
|
flexWrap: "wrap"
|
|
815
879
|
};
|
|
880
|
+
var sectionHeader = {
|
|
881
|
+
display: "flex",
|
|
882
|
+
alignItems: "flex-start",
|
|
883
|
+
justifyContent: "space-between",
|
|
884
|
+
gap: 12,
|
|
885
|
+
borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.22))",
|
|
886
|
+
paddingBottom: 6
|
|
887
|
+
};
|
|
888
|
+
var primaryTitle = { margin: 0, lineHeight: 1.3 };
|
|
889
|
+
var secondaryTitle = {
|
|
890
|
+
margin: 0,
|
|
891
|
+
lineHeight: 1.3,
|
|
892
|
+
textTransform: "uppercase",
|
|
893
|
+
letterSpacing: "0.07em",
|
|
894
|
+
opacity: 0.75
|
|
895
|
+
};
|
|
896
|
+
var sectionToggle = {
|
|
897
|
+
appearance: "none",
|
|
898
|
+
background: "transparent",
|
|
899
|
+
border: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
|
|
900
|
+
borderRadius: 3,
|
|
901
|
+
color: "inherit",
|
|
902
|
+
font: "inherit",
|
|
903
|
+
fontSize: "0.8em",
|
|
904
|
+
padding: "2px 9px",
|
|
905
|
+
cursor: "pointer",
|
|
906
|
+
flex: "0 0 auto"
|
|
907
|
+
};
|
|
908
|
+
var splitGrid = {
|
|
909
|
+
display: "grid",
|
|
910
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(min(26rem, 100%), 1fr))",
|
|
911
|
+
gap: 24,
|
|
912
|
+
alignItems: "start"
|
|
913
|
+
};
|
|
816
914
|
var filterInput = {
|
|
817
915
|
font: "inherit",
|
|
818
916
|
fontSize: "0.85em",
|
|
@@ -1668,7 +1766,6 @@ var OLDER_ROUTE = "This site\u2019s analytics route is older than this figure \u
|
|
|
1668
1766
|
function maxOf(metrics) {
|
|
1669
1767
|
return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
|
|
1670
1768
|
}
|
|
1671
|
-
var sectionHeading = { margin: "0 0 2px", lineHeight: 1.3 };
|
|
1672
1769
|
function finiteOrNull(value) {
|
|
1673
1770
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1674
1771
|
}
|
|
@@ -1770,7 +1867,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1770
1867
|
] }) })
|
|
1771
1868
|
] }),
|
|
1772
1869
|
(data.crossSource?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1773
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1870
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Everything, on one time axis" }),
|
|
1774
1871
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Each row has its own scale. Hover a day, or use the control below, to see what each source saw of it." }),
|
|
1775
1872
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1776
1873
|
CrossSourceTimeline,
|
|
@@ -1954,7 +2051,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1954
2051
|
)
|
|
1955
2052
|
] }),
|
|
1956
2053
|
(data.campaigns?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1957
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2054
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Email campaigns" }),
|
|
1958
2055
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Clicks are distinct subscribers. Opens are inflated by Apple Mail Privacy Protection, which fetches images on the recipient\u2019s behalf \u2014 so sort on clicks, not opens." }),
|
|
1959
2056
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
|
|
1960
2057
|
"The last two columns count what your order book records in the ",
|
|
@@ -2068,7 +2165,7 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
2068
2165
|
const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
|
|
2069
2166
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
2070
2167
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2071
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2168
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Pageviews, source against source" }),
|
|
2072
2169
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "The same unit on both sides. Vercel is cookieless and ungated; GA4 is consent-gated and blockable, so GA4 seeing fewer is expected." }),
|
|
2073
2170
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, outOf: "Blocked less than GA4, but not immune to it." }),
|
|
2074
2171
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, outOf: "A subset of the bar above, not a rival measurement." })
|
|
@@ -2078,7 +2175,7 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
2078
2175
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretation })
|
|
2079
2176
|
] }) }),
|
|
2080
2177
|
((data.capture?.estimates?.length ?? 0) > 0 || data.estimatedSessions?.status === "estimated") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2081
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2178
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How much GA4 is seeing" }),
|
|
2082
2179
|
/* @__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: [
|
|
2083
2180
|
/* @__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" }),
|
|
2084
2181
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatPercent(estimate.rate, 0) }),
|
|
@@ -2101,36 +2198,52 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
2101
2198
|
] }) })
|
|
2102
2199
|
] }),
|
|
2103
2200
|
Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2104
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2201
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Order statuses in this range", tone: "secondary" }),
|
|
2105
2202
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "What the orders actually say, before any filtering. Use these values to set which statuses count as a sale." }),
|
|
2106
2203
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: Object.entries(data.orderStatuses ?? {}).sort((a, b) => b[1] - a[1]).map(([status, count]) => /* @__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: [
|
|
2107
2204
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: status }),
|
|
2108
2205
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 3, children: formatCount(count) })
|
|
2109
2206
|
] }) }, status)) })
|
|
2110
2207
|
] }),
|
|
2111
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2208
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2209
|
+
Section,
|
|
2210
|
+
{
|
|
2211
|
+
title: "Context",
|
|
2212
|
+
tone: "secondary",
|
|
2213
|
+
subtitle: "Different units to the figures above, and to each other.",
|
|
2214
|
+
collapsible: true,
|
|
2215
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: cardGrid, children: [
|
|
2216
|
+
/* @__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: [
|
|
2217
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Vercel visitors" }),
|
|
2218
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelVisitors, OLDER_ROUTE), label: "Vercel visitors" }),
|
|
2219
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s own counter. Blocked far less often than Google Analytics, but not never \u2014 so read it as a floor on your real traffic." })
|
|
2220
|
+
] }) }),
|
|
2221
|
+
/* @__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: [
|
|
2222
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "GA4 sessions" }),
|
|
2223
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Sessions, label: "GA4 sessions" })
|
|
2224
|
+
] }) }),
|
|
2225
|
+
/* @__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: [
|
|
2226
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Consent granted" }),
|
|
2227
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, share of visitors GA4 saw", unit: "percent" })
|
|
2228
|
+
] }) })
|
|
2229
|
+
] })
|
|
2230
|
+
}
|
|
2231
|
+
),
|
|
2232
|
+
diagnostics && /* Open, and primary. This is the longest block on the tab and the obvious candidate
|
|
2233
|
+
for folding away — but the tab it sits on exists to answer "what should I fix
|
|
2234
|
+
before trusting any of this", and these checks ARE that answer. Folding it would
|
|
2235
|
+
have left Data health opening on a summary of the problem with the solution behind
|
|
2236
|
+
a click. It stays collapsible so a reader who has read it can get it out of the
|
|
2237
|
+
way. */
|
|
2238
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2239
|
+
Section,
|
|
2240
|
+
{
|
|
2241
|
+
title: "Configuration",
|
|
2242
|
+
subtitle: "What this site has wired up, and what it is missing.",
|
|
2243
|
+
collapsible: true,
|
|
2244
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: diagnostics })
|
|
2245
|
+
}
|
|
2246
|
+
)
|
|
2134
2247
|
] });
|
|
2135
2248
|
}
|
|
2136
2249
|
function AcquisitionPanel({ data, previous }) {
|
|
@@ -2185,7 +2298,7 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2185
2298
|
] }) })
|
|
2186
2299
|
] }),
|
|
2187
2300
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2188
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2301
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Traffic sources" }),
|
|
2189
2302
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Sort, filter, or exclude a row to see what the rest looks like." }),
|
|
2190
2303
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2191
2304
|
SortableTable,
|
|
@@ -2331,69 +2444,74 @@ function JourneyPanel({ data }) {
|
|
|
2331
2444
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
2332
2445
|
] }) }),
|
|
2333
2446
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
|
|
2334
|
-
|
|
2335
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2447
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
|
|
2448
|
+
(data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2449
|
+
Section,
|
|
2450
|
+
{
|
|
2451
|
+
title: "Other outcomes",
|
|
2452
|
+
subtitle: "Successful outcomes that are not a licence sale, counted over the same window as the funnel but not a step within it.",
|
|
2453
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: (data.outcomes ?? []).map((outcome) => /* @__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: [
|
|
2454
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: outcome.label }),
|
|
2455
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: outcome.count, label: outcome.label, size: 4 }),
|
|
2456
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: outcome.note })
|
|
2457
|
+
] }) }, outcome.key)) })
|
|
2458
|
+
}
|
|
2459
|
+
),
|
|
2460
|
+
(data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Section, { title: "Where sessions began", subtitle: "The page a visit started on, busiest first.", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LandingPagesTable, { rows: data.topLandingPages ?? [] }) })
|
|
2342
2461
|
] }),
|
|
2343
2462
|
hiddenSteps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
|
|
2344
2463
|
"Not shown: ",
|
|
2345
2464
|
hiddenSteps.map((step) => step.label.toLowerCase()).join(", "),
|
|
2346
2465
|
" \u2014 not instrumented on this site, so there is no figure to place in the funnel."
|
|
2347
|
-
] }),
|
|
2348
|
-
(data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2349
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Where sessions began" }),
|
|
2350
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2351
|
-
SortableTable,
|
|
2352
|
-
{
|
|
2353
|
-
caption: "Landing pages by sessions",
|
|
2354
|
-
initialSort: "sessions",
|
|
2355
|
-
rows: data.topLandingPages ?? [],
|
|
2356
|
-
rowKey: (page) => page.path,
|
|
2357
|
-
filterOn: (page) => page.path,
|
|
2358
|
-
filterPlaceholder: "Filter pages",
|
|
2359
|
-
exportName: "landing-pages",
|
|
2360
|
-
columns: [
|
|
2361
|
-
{
|
|
2362
|
-
key: "path",
|
|
2363
|
-
label: "Page",
|
|
2364
|
-
sortValue: (page) => page.path,
|
|
2365
|
-
// The comment here used to describe this fix without applying it: it said
|
|
2366
|
-
// the foundry's own URLs were the one table that did not link while
|
|
2367
|
-
// referrer hosts did, and then rendered plain text. A comment asserting
|
|
2368
|
-
// something the code does not do is worse than no comment.
|
|
2369
|
-
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: page.path, target: "_blank", rel: "noopener noreferrer", style: sourceLink, children: page.path }) })
|
|
2370
|
-
},
|
|
2371
|
-
{
|
|
2372
|
-
key: "sessions",
|
|
2373
|
-
label: "Sessions",
|
|
2374
|
-
numeric: true,
|
|
2375
|
-
sortValue: (page) => page.sessions,
|
|
2376
|
-
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(page.sessions) })
|
|
2377
|
-
},
|
|
2378
|
-
{
|
|
2379
|
-
key: "engagement",
|
|
2380
|
-
label: "Engaged",
|
|
2381
|
-
numeric: true,
|
|
2382
|
-
sortValue: (page) => finiteOrNull(page.engagementRate),
|
|
2383
|
-
exportValue: (page) => {
|
|
2384
|
-
const r = finiteOrNull(page.engagementRate);
|
|
2385
|
-
return r === null ? null : formatPercent(r, 0);
|
|
2386
|
-
},
|
|
2387
|
-
// Volume alone could not distinguish a page that delivers 200 arrivals
|
|
2388
|
-
// which leave from one that feeds the shop.
|
|
2389
|
-
render: (page) => finiteOrNull(page.engagementRate) === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatPercent(page.engagementRate, 0) })
|
|
2390
|
-
}
|
|
2391
|
-
]
|
|
2392
|
-
}
|
|
2393
|
-
)
|
|
2394
2466
|
] })
|
|
2395
2467
|
] });
|
|
2396
2468
|
}
|
|
2469
|
+
function LandingPagesTable({ rows }) {
|
|
2470
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2471
|
+
SortableTable,
|
|
2472
|
+
{
|
|
2473
|
+
caption: "Landing pages by sessions",
|
|
2474
|
+
initialSort: "sessions",
|
|
2475
|
+
rows,
|
|
2476
|
+
rowKey: (page) => page.path,
|
|
2477
|
+
filterOn: (page) => page.path,
|
|
2478
|
+
filterPlaceholder: "Filter pages",
|
|
2479
|
+
exportName: "landing-pages",
|
|
2480
|
+
columns: [
|
|
2481
|
+
{
|
|
2482
|
+
key: "path",
|
|
2483
|
+
label: "Page",
|
|
2484
|
+
sortValue: (page) => page.path,
|
|
2485
|
+
// The comment here used to describe this fix without applying it: it said
|
|
2486
|
+
// the foundry's own URLs were the one table that did not link while
|
|
2487
|
+
// referrer hosts did, and then rendered plain text. A comment asserting
|
|
2488
|
+
// something the code does not do is worse than no comment.
|
|
2489
|
+
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: page.path, target: "_blank", rel: "noopener noreferrer", style: sourceLink, children: page.path }) })
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
key: "sessions",
|
|
2493
|
+
label: "Sessions",
|
|
2494
|
+
numeric: true,
|
|
2495
|
+
sortValue: (page) => page.sessions,
|
|
2496
|
+
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(page.sessions) })
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
key: "engagement",
|
|
2500
|
+
label: "Engaged",
|
|
2501
|
+
numeric: true,
|
|
2502
|
+
sortValue: (page) => finiteOrNull(page.engagementRate),
|
|
2503
|
+
exportValue: (page) => {
|
|
2504
|
+
const r = finiteOrNull(page.engagementRate);
|
|
2505
|
+
return r === null ? null : formatPercent(r, 0);
|
|
2506
|
+
},
|
|
2507
|
+
// Volume alone could not distinguish a page that delivers 200 arrivals
|
|
2508
|
+
// which leave from one that feeds the shop.
|
|
2509
|
+
render: (page) => finiteOrNull(page.engagementRate) === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatPercent(page.engagementRate, 0) })
|
|
2510
|
+
}
|
|
2511
|
+
]
|
|
2512
|
+
}
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2397
2515
|
var MIN_FAMILY_VIEWS = 30;
|
|
2398
2516
|
function catalogueRate(rows) {
|
|
2399
2517
|
let views = 0;
|
|
@@ -2450,7 +2568,7 @@ function TypefaceInterestPanel({ data }) {
|
|
|
2450
2568
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
2451
2569
|
/* @__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.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
|
|
2452
2570
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2453
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2571
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Engagement by typeface" }),
|
|
2454
2572
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
|
|
2455
2573
|
"Sort by any column. \u201CSells vs catalogue\u201D compares each family\u2019s sales-per-view against the catalogue as a whole \u2014 1.0\xD7 is average, and sorting up finds the families that are looked at and do not sell. It is a comparison between your families, not the share of visitors who buy: Google Analytics sees only a fraction of the views, which moves every family together and so cancels out here. With one or two sales a family will still swing a long way, so read it alongside the order counts beside it.",
|
|
2456
2574
|
" ",
|
|
@@ -2585,7 +2703,7 @@ function TypefaceInterestPanel({ data }) {
|
|
|
2585
2703
|
)
|
|
2586
2704
|
] }),
|
|
2587
2705
|
(data.licences?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2588
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2706
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How licences sell" }),
|
|
2589
2707
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "From the orders themselves, so this is exact and unaffected by whatever GA4 is doing. Tier and term are separate rows, because they are the two variables in the pricing question and a tier that sells well at one year may not at perpetual." }),
|
|
2590
2708
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2591
2709
|
ProportionChart,
|