@liiift-studio/sanity-visitor-insights 0.64.0 → 0.66.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 CHANGED
@@ -307,7 +307,13 @@ 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" },
314
+ "shift.now": { key: "vercel", alpha: 1, role: "data", note: "A channel this period" },
315
+ "shift.before": { key: "neutral", alpha: 0.55, role: "furniture", note: "The same channel last period \u2014 context, not a second answer" },
316
+ "shift.link": { key: "neutral", alpha: 0.35, role: "furniture", note: "The distance between the two, which is the finding" }
311
317
  };
312
318
  function mark(name) {
313
319
  const m = MARKS[name];
@@ -846,6 +852,50 @@ function ContainmentBar({
846
852
  ] })
847
853
  ] });
848
854
  }
855
+ function EstimateDotPlot({
856
+ estimates,
857
+ labelFor,
858
+ intervalFor
859
+ }) {
860
+ if (estimates.length === 0) return null;
861
+ const intervals = estimates.map((e) => intervalFor(e));
862
+ const top = Math.max(1, ...estimates.map((e) => e.rate), ...intervals.map((i) => i.high));
863
+ const pct = (v) => `${Math.max(0, Math.min(1, v / top)) * 100}%`;
864
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
865
+ estimates.map((estimate, index) => {
866
+ const interval = intervals[index];
867
+ const wide = interval.high - interval.low > 0.4;
868
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
869
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
870
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: labelFor(estimate.basis) }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: formatPercent(estimate.rate, 0) })
872
+ ] }),
873
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "aria-hidden": "true", style: estimateTrack, children: [
874
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...estimateRule, left: pct(1) } }),
875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
876
+ "div",
877
+ {
878
+ style: {
879
+ ...estimateInterval,
880
+ left: pct(interval.low),
881
+ width: `calc(${pct(interval.high)} - ${pct(interval.low)})`
882
+ }
883
+ }
884
+ ),
885
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...estimateDot, left: pct(estimate.rate) } })
886
+ ] }),
887
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
888
+ formatCount(estimate.observed),
889
+ " of ",
890
+ formatCount(estimate.actual),
891
+ wide && " \u2014 too small a sample to lean on",
892
+ 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."
893
+ ] })
894
+ ] }, estimate.basis);
895
+ }),
896
+ /* @__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%." })
897
+ ] });
898
+ }
849
899
  function SortableTable({
850
900
  caption,
851
901
  columns,
@@ -1063,6 +1113,66 @@ var splitGrid = {
1063
1113
  gap: 24,
1064
1114
  alignItems: "start"
1065
1115
  };
1116
+ var estimateTrack = {
1117
+ position: "relative",
1118
+ height: 18,
1119
+ borderRadius: 2,
1120
+ background: mark("bar.track")
1121
+ };
1122
+ var estimateRule = {
1123
+ position: "absolute",
1124
+ top: 0,
1125
+ bottom: 0,
1126
+ width: 1,
1127
+ background: mark("estimate.rule")
1128
+ };
1129
+ var estimateInterval = {
1130
+ position: "absolute",
1131
+ top: 6,
1132
+ height: 6,
1133
+ borderRadius: 3,
1134
+ background: mark("estimate.interval")
1135
+ };
1136
+ var estimateDot = {
1137
+ position: "absolute",
1138
+ top: 4,
1139
+ width: 10,
1140
+ height: 10,
1141
+ marginLeft: -5,
1142
+ borderRadius: "50%",
1143
+ background: mark("estimate.dot")
1144
+ };
1145
+ var shiftTrack = {
1146
+ position: "relative",
1147
+ height: 16,
1148
+ borderRadius: 2,
1149
+ background: mark("bar.track")
1150
+ };
1151
+ var shiftLink = {
1152
+ position: "absolute",
1153
+ top: 7,
1154
+ height: 2,
1155
+ background: mark("shift.link")
1156
+ };
1157
+ var shiftBefore = {
1158
+ position: "absolute",
1159
+ top: 3,
1160
+ width: 10,
1161
+ height: 10,
1162
+ marginLeft: -5,
1163
+ borderRadius: "50%",
1164
+ border: `2px solid ${mark("shift.before")}`,
1165
+ background: "transparent"
1166
+ };
1167
+ var shiftNow = {
1168
+ position: "absolute",
1169
+ top: 3,
1170
+ width: 10,
1171
+ height: 10,
1172
+ marginLeft: -5,
1173
+ borderRadius: "50%",
1174
+ background: mark("shift.now")
1175
+ };
1066
1176
  var filterInput = {
1067
1177
  font: "inherit",
1068
1178
  fontSize: "0.85em",
@@ -1884,6 +1994,15 @@ function describeRank(rank) {
1884
1994
  return `${ordinal(rank.rank)} best of the last ${rank.of} weeks`;
1885
1995
  }
1886
1996
 
1997
+ // src/core/capture.ts
1998
+ function samplingInterval(estimate) {
1999
+ const n = estimate.actual;
2000
+ if (!Number.isFinite(n) || n <= 0) return { low: estimate.rate, high: estimate.rate };
2001
+ const p = Math.min(1, Math.max(0, estimate.rate));
2002
+ const error = 2 * Math.sqrt(Math.max(p * (1 - p), 0.02) / n);
2003
+ return { low: Math.max(0, estimate.rate - error), high: estimate.rate + error };
2004
+ }
2005
+
1887
2006
  // src/studio/panels.tsx
1888
2007
  var import_jsx_runtime3 = require("react/jsx-runtime");
1889
2008
  function metricSortValue(metric) {
@@ -2387,18 +2506,14 @@ function DataHealthPanel({ data, diagnostics }) {
2387
2506
  ),
2388
2507
  ((data.capture?.estimates?.length ?? 0) > 0 || data.estimatedSessions?.status === "estimated") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2389
2508
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How much GA4 is seeing" }),
2390
- /* @__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: [
2391
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: estimate.basis === "orders" ? "Checked against your orders" : estimate.basis === "email" ? "Checked against email clicks" : "Checked against Vercel" }),
2392
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatPercent(estimate.rate, 0) }),
2393
- estimate.rate > 1.05 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, children: "Above 100%: GA4 is counting more than the source it is measured against. That is usually a tag firing twice, not extra traffic." }),
2394
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2395
- formatCount(estimate.observed),
2396
- " of ",
2397
- formatCount(estimate.actual),
2398
- ". ",
2399
- estimate.note
2400
- ] })
2401
- ] }) }, estimate.basis)) }),
2509
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2510
+ EstimateDotPlot,
2511
+ {
2512
+ estimates: data.capture?.estimates ?? [],
2513
+ labelFor: (basis) => basis === "orders" ? "Checked against your orders" : basis === "email" ? "Checked against email clicks" : "Checked against Vercel",
2514
+ intervalFor: samplingInterval
2515
+ }
2516
+ ),
2402
2517
  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: [
2403
2518
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: "The sources disagree, and that is informative" }),
2404
2519
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: data.capture.discrepancy })