@liiift-studio/sanity-visitor-insights 0.63.0 → 0.65.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.js +140 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +140 -29
- package/dist/index.mjs.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/capture.ts +1 -1
- package/src/studio/Figure.tsx +136 -0
- package/src/studio/palette.ts +3 -0
- package/src/studio/panels.test.tsx +148 -1
- package/src/studio/panels.tsx +78 -46
package/dist/index.js
CHANGED
|
@@ -307,7 +307,10 @@ var MARKS = {
|
|
|
307
307
|
"bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
|
|
308
308
|
"bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
|
|
309
309
|
"bar.lost": { key: "revenue", alpha: 1, role: "data", note: "People who did not continue past a funnel rung" },
|
|
310
|
-
"bar.seen": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The share of a complete count that the lossy source saw" }
|
|
310
|
+
"bar.seen": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The share of a complete count that the lossy source saw" },
|
|
311
|
+
"estimate.dot": { key: "ga4Pageviews", alpha: 1, role: "data", note: "One independent estimate of how much GA4 sees" },
|
|
312
|
+
"estimate.interval": { key: "ga4Pageviews", alpha: 0.35, role: "fill", boundary: "estimate.dot", note: "How wide the sample leaves that estimate \u2014 the dot is its own boundary" },
|
|
313
|
+
"estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" }
|
|
311
314
|
};
|
|
312
315
|
function mark(name) {
|
|
313
316
|
const m = MARKS[name];
|
|
@@ -846,6 +849,50 @@ function ContainmentBar({
|
|
|
846
849
|
] })
|
|
847
850
|
] });
|
|
848
851
|
}
|
|
852
|
+
function EstimateDotPlot({
|
|
853
|
+
estimates,
|
|
854
|
+
labelFor,
|
|
855
|
+
intervalFor
|
|
856
|
+
}) {
|
|
857
|
+
if (estimates.length === 0) return null;
|
|
858
|
+
const intervals = estimates.map((e) => intervalFor(e));
|
|
859
|
+
const top = Math.max(1, ...estimates.map((e) => e.rate), ...intervals.map((i) => i.high));
|
|
860
|
+
const pct = (v) => `${Math.max(0, Math.min(1, v / top)) * 100}%`;
|
|
861
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
862
|
+
estimates.map((estimate, index) => {
|
|
863
|
+
const interval = intervals[index];
|
|
864
|
+
const wide = interval.high - interval.low > 0.4;
|
|
865
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
866
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
|
|
867
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: labelFor(estimate.basis) }),
|
|
868
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: formatPercent(estimate.rate, 0) })
|
|
869
|
+
] }),
|
|
870
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "aria-hidden": "true", style: estimateTrack, children: [
|
|
871
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...estimateRule, left: pct(1) } }),
|
|
872
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
873
|
+
"div",
|
|
874
|
+
{
|
|
875
|
+
style: {
|
|
876
|
+
...estimateInterval,
|
|
877
|
+
left: pct(interval.low),
|
|
878
|
+
width: `calc(${pct(interval.high)} - ${pct(interval.low)})`
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
),
|
|
882
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...estimateDot, left: pct(estimate.rate) } })
|
|
883
|
+
] }),
|
|
884
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
885
|
+
formatCount(estimate.observed),
|
|
886
|
+
" of ",
|
|
887
|
+
formatCount(estimate.actual),
|
|
888
|
+
wide && " \u2014 too small a sample to lean on",
|
|
889
|
+
estimate.rate > 1.05 && " \u2014 over 100%, so GA4 is counting more than the source it is checked against. That is usually a tag firing twice, not extra traffic."
|
|
890
|
+
] })
|
|
891
|
+
] }, estimate.basis);
|
|
892
|
+
}),
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "Each is a separate way of asking the same question. Where they disagree, the spread is the answer\u2019s uncertainty \u2014 the rule marks 100%." })
|
|
894
|
+
] });
|
|
895
|
+
}
|
|
849
896
|
function SortableTable({
|
|
850
897
|
caption,
|
|
851
898
|
columns,
|
|
@@ -1063,6 +1110,35 @@ var splitGrid = {
|
|
|
1063
1110
|
gap: 24,
|
|
1064
1111
|
alignItems: "start"
|
|
1065
1112
|
};
|
|
1113
|
+
var estimateTrack = {
|
|
1114
|
+
position: "relative",
|
|
1115
|
+
height: 18,
|
|
1116
|
+
borderRadius: 2,
|
|
1117
|
+
background: mark("bar.track")
|
|
1118
|
+
};
|
|
1119
|
+
var estimateRule = {
|
|
1120
|
+
position: "absolute",
|
|
1121
|
+
top: 0,
|
|
1122
|
+
bottom: 0,
|
|
1123
|
+
width: 1,
|
|
1124
|
+
background: mark("estimate.rule")
|
|
1125
|
+
};
|
|
1126
|
+
var estimateInterval = {
|
|
1127
|
+
position: "absolute",
|
|
1128
|
+
top: 6,
|
|
1129
|
+
height: 6,
|
|
1130
|
+
borderRadius: 3,
|
|
1131
|
+
background: mark("estimate.interval")
|
|
1132
|
+
};
|
|
1133
|
+
var estimateDot = {
|
|
1134
|
+
position: "absolute",
|
|
1135
|
+
top: 4,
|
|
1136
|
+
width: 10,
|
|
1137
|
+
height: 10,
|
|
1138
|
+
marginLeft: -5,
|
|
1139
|
+
borderRadius: "50%",
|
|
1140
|
+
background: mark("estimate.dot")
|
|
1141
|
+
};
|
|
1066
1142
|
var filterInput = {
|
|
1067
1143
|
font: "inherit",
|
|
1068
1144
|
fontSize: "0.85em",
|
|
@@ -1884,6 +1960,15 @@ function describeRank(rank) {
|
|
|
1884
1960
|
return `${ordinal(rank.rank)} best of the last ${rank.of} weeks`;
|
|
1885
1961
|
}
|
|
1886
1962
|
|
|
1963
|
+
// src/core/capture.ts
|
|
1964
|
+
function samplingInterval(estimate) {
|
|
1965
|
+
const n = estimate.actual;
|
|
1966
|
+
if (!Number.isFinite(n) || n <= 0) return { low: estimate.rate, high: estimate.rate };
|
|
1967
|
+
const p = Math.min(1, Math.max(0, estimate.rate));
|
|
1968
|
+
const error = 2 * Math.sqrt(Math.max(p * (1 - p), 0.02) / n);
|
|
1969
|
+
return { low: Math.max(0, estimate.rate - error), high: estimate.rate + error };
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1887
1972
|
// src/studio/panels.tsx
|
|
1888
1973
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1889
1974
|
function metricSortValue(metric) {
|
|
@@ -2074,38 +2159,62 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
2074
2159
|
source: "GA4",
|
|
2075
2160
|
complete: true,
|
|
2076
2161
|
unit: "percent",
|
|
2077
|
-
// Fixed
|
|
2078
|
-
//
|
|
2079
|
-
|
|
2162
|
+
// Fixed at the bottom, open at the top.
|
|
2163
|
+
//
|
|
2164
|
+
// The lower bound is fixed so a site at a flat 20% cannot scale itself
|
|
2165
|
+
// to full height and read as healthy. The UPPER bound is not clamped,
|
|
2166
|
+
// because over-counting is a real fault with its own interpretation
|
|
2167
|
+
// branch — and the card further down this very file refuses the same
|
|
2168
|
+
// clamp in prose, saying `Math.min(1, …)` rendered a tag reporting
|
|
2169
|
+
// 250% of reality as a flat 100% under the heading "How much GA4 is
|
|
2170
|
+
// seeing", i.e. perfect. One quantity, two opposite policies, one
|
|
2171
|
+
// file: the row drew a double-firing tag as a healthy flat line while
|
|
2172
|
+
// the sentence beside it called the over-count out.
|
|
2173
|
+
domain: [0, Math.max(1, ...(data.crossSource ?? []).map((d) => d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? d.ga4Pageviews / d.vercelPageviews : 0))],
|
|
2080
2174
|
points: (data.crossSource ?? []).map((d) => ({
|
|
2081
2175
|
date: d.date,
|
|
2082
|
-
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ?
|
|
2176
|
+
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? d.ga4Pageviews / d.vercelPageviews : null
|
|
2083
2177
|
})),
|
|
2084
2178
|
// The most valuable ghost in the chart: a collapse reads as a step away
|
|
2085
2179
|
// from last period's flat line, whether or not the detector fired.
|
|
2086
2180
|
comparison: (previous?.crossSource ?? []).map((d) => ({
|
|
2087
2181
|
date: d.date,
|
|
2088
|
-
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ?
|
|
2182
|
+
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? d.ga4Pageviews / d.vercelPageviews : null
|
|
2089
2183
|
}))
|
|
2090
2184
|
}] : [],
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2185
|
+
// ORDERS ALWAYS, revenue only when it covers every order.
|
|
2186
|
+
//
|
|
2187
|
+
// This used to draw revenue whenever ANY day had a revenue figure, and
|
|
2188
|
+
// otherwise orders — never both. Combined with the server filling a day's
|
|
2189
|
+
// revenue as `revenueByDate[date] ?? 0`, that meant a day carrying a real
|
|
2190
|
+
// order with no recorded amount arrived as a measured ZERO, and the row drew
|
|
2191
|
+
// a zero-tick on it: the mark whose own comment says "measured, and it was
|
|
2192
|
+
// nothing".
|
|
2193
|
+
//
|
|
2194
|
+
// At Darden 11 of 69 orders carry an amount, so the flagship chart was
|
|
2195
|
+
// asserting that no sale happened on 58 days that had sales — the package's
|
|
2196
|
+
// founding distinction inverted inside its most prominent encoding. Orders
|
|
2197
|
+
// are exact and complete whatever the amounts say, so they are the row that
|
|
2198
|
+
// is always honest.
|
|
2199
|
+
{
|
|
2101
2200
|
key: "orders",
|
|
2102
2201
|
label: "Orders",
|
|
2103
2202
|
source: "Sanity",
|
|
2104
2203
|
complete: true,
|
|
2105
2204
|
unit: "count",
|
|
2205
|
+
// A sale is an event on a date, not a level that varies day to day.
|
|
2106
2206
|
mark: "events",
|
|
2107
2207
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.orders }))
|
|
2108
|
-
}
|
|
2208
|
+
},
|
|
2209
|
+
...revenueCoversEveryOrder(data) ? [{
|
|
2210
|
+
key: "revenue",
|
|
2211
|
+
label: "Revenue",
|
|
2212
|
+
source: "Sanity",
|
|
2213
|
+
complete: true,
|
|
2214
|
+
unit: "money",
|
|
2215
|
+
mark: "events",
|
|
2216
|
+
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.revenue }))
|
|
2217
|
+
}] : []
|
|
2109
2218
|
]
|
|
2110
2219
|
}
|
|
2111
2220
|
),
|
|
@@ -2363,18 +2472,14 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
2363
2472
|
),
|
|
2364
2473
|
((data.capture?.estimates?.length ?? 0) > 0 || data.estimatedSessions?.status === "estimated") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2365
2474
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How much GA4 is seeing" }),
|
|
2366
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
". ",
|
|
2375
|
-
estimate.note
|
|
2376
|
-
] })
|
|
2377
|
-
] }) }, estimate.basis)) }),
|
|
2475
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2476
|
+
EstimateDotPlot,
|
|
2477
|
+
{
|
|
2478
|
+
estimates: data.capture?.estimates ?? [],
|
|
2479
|
+
labelFor: (basis) => basis === "orders" ? "Checked against your orders" : basis === "email" ? "Checked against email clicks" : "Checked against Vercel",
|
|
2480
|
+
intervalFor: samplingInterval
|
|
2481
|
+
}
|
|
2482
|
+
),
|
|
2378
2483
|
data.capture?.discrepancy && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
2379
2484
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: "The sources disagree, and that is informative" }),
|
|
2380
2485
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: data.capture.discrepancy })
|
|
@@ -2679,6 +2784,12 @@ function SegmentTable({ segments, dimension }) {
|
|
|
2679
2784
|
] }) })
|
|
2680
2785
|
] });
|
|
2681
2786
|
}
|
|
2787
|
+
function revenueCoversEveryOrder(data) {
|
|
2788
|
+
const counted = metricSortValue(data.orders);
|
|
2789
|
+
const withTotal = finiteOrNull(data.ordersWithTotal);
|
|
2790
|
+
if (counted === null || withTotal === null) return false;
|
|
2791
|
+
return counted === withTotal;
|
|
2792
|
+
}
|
|
2682
2793
|
function JourneyPanel({ data }) {
|
|
2683
2794
|
const segments = data.segments ?? [];
|
|
2684
2795
|
const allSteps = data.steps ?? [];
|