@liiift-studio/sanity-visitor-insights 0.64.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 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) {
@@ -2387,18 +2472,14 @@ function DataHealthPanel({ data, diagnostics }) {
2387
2472
  ),
2388
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: [
2389
2474
  /* @__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)) }),
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
+ ),
2402
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: [
2403
2484
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: "The sources disagree, and that is informative" }),
2404
2485
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: data.capture.discrepancy })