@liiift-studio/sanity-visitor-insights 0.30.0 → 0.31.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/server.js CHANGED
@@ -1057,7 +1057,7 @@ function fromOrders(ga4Purchases, sanityOrders) {
1057
1057
  rate: ga4Purchases / sanityOrders,
1058
1058
  observed: ga4Purchases,
1059
1059
  actual: sanityOrders,
1060
- note: `GA4 purchases against the ${sanityOrders} orders that exist \u2014 the same event on both sides. Small numbers move it a long way: one more or fewer order shifts this rate by about ${swing} points.`
1060
+ note: `GA4 purchases against the ${sanityOrders} orders that exist \u2014 the same event on both sides. Small numbers move it a long way: one more or fewer purchase seen by GA4 shifts this rate by about ${swing} points.`
1061
1061
  };
1062
1062
  }
1063
1063
  function fromPageviews(ga4Pageviews, vercelPageviews) {
@@ -1080,6 +1080,13 @@ function fromEmail(ga4Sessions, mailchimpClicks) {
1080
1080
  note: "GA4 sessions from the campaign against Mailchimp\u2019s unique clicks. A cohort whose true size is known exactly."
1081
1081
  };
1082
1082
  }
1083
+ function samplingInterval(estimate) {
1084
+ const n = estimate.actual;
1085
+ if (!Number.isFinite(n) || n <= 0) return { low: estimate.rate, high: estimate.rate };
1086
+ const p = Math.min(1, Math.max(0, estimate.rate));
1087
+ const error = 2 * Math.sqrt(Math.max(p * (1 - p), 0.02) / n);
1088
+ return { low: Math.max(0, p - error), high: Math.min(1, p + error) };
1089
+ }
1083
1090
  var TRUST = ["orders", "email", "pageviews"];
1084
1091
  function captureModel(candidates) {
1085
1092
  const estimates = candidates.filter((e) => e !== null).sort((a, b) => TRUST.indexOf(a.basis) - TRUST.indexOf(b.basis));
@@ -1088,9 +1095,8 @@ function captureModel(candidates) {
1088
1095
  }
1089
1096
  const rates = estimates.map((e) => e.rate);
1090
1097
  const rate = estimates[0].rate;
1091
- const low = Math.min(...rates);
1092
- const high = Math.max(...rates);
1093
- return { estimates, rate, low, high, discrepancy: describeDisagreement(estimates) };
1098
+ const spread = estimates.length > 1 ? { low: Math.min(...rates), high: Math.max(...rates) } : samplingInterval(estimates[0]);
1099
+ return { estimates, rate, low: spread.low, high: spread.high, discrepancy: describeDisagreement(estimates) };
1094
1100
  }
1095
1101
  function describeDisagreement(estimates) {
1096
1102
  if (estimates.length < 2) return null;
@@ -1147,7 +1153,7 @@ function interpret(ga4Views, vercelViews, shortfall, consent) {
1147
1153
  return `${base} How much of that is consent refusal cannot be measured until the consent_granted event is instrumented, so the difference is currently unexplained rather than attributed.`;
1148
1154
  }
1149
1155
  const consentPercent = Math.round((1 - consent.value / 100) * 100);
1150
- return `${base} Around ${consentPercent}% of sessions did not grant analytics consent, which accounts for part of it. The remainder is unexplained \u2014 ad-blocking and bot filtering are plausible but are not separately measurable.`;
1156
+ return `${base} Of the visitors GA4 did record, at least ${consentPercent}% declined analytics consent. That is measured inside GA4's own sample, so it does not explain the visitors missing from it \u2014 anyone who refused before being counted is in neither figure. The gap itself stays unexplained; ad-blocking and bot filtering are plausible and are not separately measurable.`;
1151
1157
  }
1152
1158
  function calendarDays(start, end) {
1153
1159
  const first = Date.parse(`${start}T00:00:00Z`);
@@ -1316,9 +1322,18 @@ async function measurementHealth(input) {
1316
1322
  revenue = unavailable("source_error");
1317
1323
  }
1318
1324
  }
1319
- const shortfallRatio = ga4Pageviews.status !== "unavailable" && vercelPageviews.status !== "unavailable" && vercelPageviews.value > 0 ? (vercelPageviews.value - ga4Pageviews.value) / vercelPageviews.value : null;
1320
1325
  const vercelDates = Object.keys(vercelByDate);
1321
- const vercelIsDaily = vercelDates.length === 0 || vercelDates.length > daysInRange(range) * 0.7;
1326
+ const vercelIsDaily = vercelDates.length > 0 && vercelDates.length > daysInRange(range) * 0.7;
1327
+ const unsettled = new Set(provisionalDates(range));
1328
+ const settledGa4 = [...ga4ByDate.entries()].filter(([date]) => !unsettled.has(date));
1329
+ const settledVercel = vercelIsDaily ? Object.entries(vercelByDate).filter(([date]) => !unsettled.has(date)) : [];
1330
+ const settledShortfall = (() => {
1331
+ if (settledGa4.length === 0 || settledVercel.length === 0) return null;
1332
+ const ga4Total = settledGa4.reduce((sum, [, value]) => sum + value, 0);
1333
+ const vercelTotal = settledVercel.reduce((sum, [, value]) => sum + (value ?? 0), 0);
1334
+ return vercelTotal > 0 ? (vercelTotal - ga4Total) / vercelTotal : null;
1335
+ })();
1336
+ const shortfallRatio = settledShortfall !== null ? settledShortfall : ga4Pageviews.status !== "unavailable" && vercelPageviews.status !== "unavailable" && vercelPageviews.value > 0 ? (vercelPageviews.value - ga4Pageviews.value) / vercelPageviews.value : null;
1322
1337
  const seriesDates = vercelIsDaily ? Array.from(/* @__PURE__ */ new Set([...ga4ByDate.keys(), ...vercelDates])) : Array.from(ga4ByDate.keys());
1323
1338
  const daily = seriesDates.sort().map((date) => ({
1324
1339
  date,