@liiift-studio/sanity-visitor-insights 0.70.0 → 0.71.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CMADS44I.mjs';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CMADS44I.mjs';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CMhL0Chn.mjs';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CMhL0Chn.mjs';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
@@ -206,14 +206,21 @@ interface MetricFigureProps {
206
206
  * suffix inverted the conclusion.
207
207
  *
208
208
  * 'percent' expects a 0-100 value, matching what measurementHealth produces.
209
+ *
210
+ * 'money' writes the figure in the range's currency. Without it a PARTIAL revenue reached the
211
+ * count formatter, so Darden's default tab — where revenue is permanently partial, because most
212
+ * orders predate the amount field — rendered its headline money as a bare "12,346", beside an
213
+ * "Average order US$455" that did carry a currency.
209
214
  */
210
- unit?: 'count' | 'percent';
215
+ unit?: 'count' | 'percent' | 'money';
216
+ /** ISO 4217 code. Read only when `unit` is 'money'. */
217
+ currency?: string | null;
211
218
  }
212
219
  /**
213
220
  * Render a metric value, handling the absent case visibly.
214
221
  * Screen readers get the reason text rather than an unexplained dash.
215
222
  */
216
- declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
223
+ declare function MetricFigure({ metric, label, size, unit, currency }: MetricFigureProps): React.ReactElement;
217
224
  /** Props for ChartData. */
218
225
  interface ChartDataProps<Row> {
219
226
  /** What the disclosure is called. Names the chart it belongs to. */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CMADS44I.js';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CMADS44I.js';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CMhL0Chn.js';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CMhL0Chn.js';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
@@ -206,14 +206,21 @@ interface MetricFigureProps {
206
206
  * suffix inverted the conclusion.
207
207
  *
208
208
  * 'percent' expects a 0-100 value, matching what measurementHealth produces.
209
+ *
210
+ * 'money' writes the figure in the range's currency. Without it a PARTIAL revenue reached the
211
+ * count formatter, so Darden's default tab — where revenue is permanently partial, because most
212
+ * orders predate the amount field — rendered its headline money as a bare "12,346", beside an
213
+ * "Average order US$455" that did carry a currency.
209
214
  */
210
- unit?: 'count' | 'percent';
215
+ unit?: 'count' | 'percent' | 'money';
216
+ /** ISO 4217 code. Read only when `unit` is 'money'. */
217
+ currency?: string | null;
211
218
  }
212
219
  /**
213
220
  * Render a metric value, handling the absent case visibly.
214
221
  * Screen readers get the reason text rather than an unexplained dash.
215
222
  */
216
- declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
223
+ declare function MetricFigure({ metric, label, size, unit, currency }: MetricFigureProps): React.ReactElement;
217
224
  /** Props for ChartData. */
218
225
  interface ChartDataProps<Row> {
219
226
  /** What the disclosure is called. Names the chart it belongs to. */
package/dist/index.js CHANGED
@@ -295,9 +295,8 @@ var COMPARISON_ON_LIGHT = "#6A4FC4";
295
295
  var COMPARISON_ON_DARK = "#A594E8";
296
296
  var COMPARISON_STYLE = `
297
297
  :root { --vi-comparison: ${COMPARISON}; }
298
- @supports (color: light-dark(#000, #fff)) {
299
- :root { --vi-comparison: light-dark(${COMPARISON_ON_LIGHT}, ${COMPARISON_ON_DARK}); }
300
- }
298
+ [data-scheme="light"] { --vi-comparison: ${COMPARISON_ON_LIGHT}; }
299
+ [data-scheme="dark"] { --vi-comparison: ${COMPARISON_ON_DARK}; }
301
300
  `.trim();
302
301
  var MARKS = {
303
302
  "chart.line": { key: "vercel", alpha: 1, role: "data", note: "A source series in the timeline" },
@@ -316,6 +315,10 @@ var MARKS = {
316
315
  "chart.ghost": { key: "comparison", alpha: 1, role: "data", note: "The same window last period, behind everything" },
317
316
  "chart.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "Grid rules" },
318
317
  "bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
318
+ // The hatch on a rung whose count covers fewer days than the rail it sits in. A `data` mark, not
319
+ // furniture: it is the only thing on that rung saying the figure is not comparable, so it has to
320
+ // be as visible as a bar would have been.
321
+ "bar.partial": { key: "vercel", alpha: 1, role: "data", note: "A funnel rung measured over part of the window" },
319
322
  "bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
320
323
  // Neither a source nor money. It was drawn in the REVENUE hue, so a magenta bar between funnel
321
324
  // rungs read as takings; GA4's hue was rejected for the opposite reason, since that hue means
@@ -366,7 +369,7 @@ function partial(value, coveredFrom, note) {
366
369
  return { status: "partial", value, coveredFrom, note };
367
370
  }
368
371
  function valueOrNull(metric) {
369
- return metric.status === "unavailable" ? null : metric.value;
372
+ return !metric || metric.status === "unavailable" ? null : metric.value;
370
373
  }
371
374
  var REPORT_NAMES = [
372
375
  "measurement-health",
@@ -530,7 +533,10 @@ var hoverLabel = { opacity: 0.75, whiteSpace: "nowrap", overflow: "hidden", text
530
533
  var hoverValue = { marginLeft: "auto", fontVariantNumeric: "tabular-nums", fontWeight: 500, whiteSpace: "nowrap" };
531
534
  var hoverSeen = { opacity: 0.6, fontWeight: 400 };
532
535
  var hoverEvent = { fontSize: 11, opacity: 0.8, borderTop: "1px solid var(--card-border-color, rgba(128,128,128,0.25))", paddingTop: 4, marginTop: 2 };
533
- function MetricFigure({ metric, label, size = 4, unit = "count" }) {
536
+ function partialBadge(metric) {
537
+ return metric.coveredFrom ? `From ${formatDay(metric.coveredFrom)} only` : "Some orders only";
538
+ }
539
+ function MetricFigure({ metric, label, size = 4, unit = "count", currency = null }) {
534
540
  if (metric.status === "unavailable") {
535
541
  const reason = REASON_TEXT[metric.reason] ?? "No figure available";
536
542
  const detail = metric.detail ? `${reason}. ${metric.detail}` : reason;
@@ -557,7 +563,7 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
557
563
  ] }) })
558
564
  );
559
565
  }
560
- const formatted = unit === "percent" ? `${metric.value.toFixed(1)}%` : formatCount(metric.value);
566
+ const formatted = unit === "percent" ? `${metric.value.toFixed(1)}%` : unit === "money" ? formatMoney(metric.value, currency) : formatCount(metric.value);
561
567
  if (metric.status === "partial") {
562
568
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
563
569
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size, children: [
@@ -567,11 +573,11 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
567
573
  metric.note
568
574
  ] })
569
575
  ] }),
570
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: "Some orders only" })
576
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: partialBadge(metric) })
571
577
  ] });
572
578
  }
573
579
  if (metric.status === "estimated") {
574
- const range = unit === "percent" ? `${metric.low.toFixed(1)}% to ${metric.high.toFixed(1)}%` : `${formatCount(metric.low)} to ${formatCount(metric.high)}`;
580
+ const range = unit === "percent" ? `${metric.low.toFixed(1)}% to ${metric.high.toFixed(1)}%` : unit === "money" ? `${formatMoney(metric.low, currency)} to ${formatMoney(metric.high, currency)}` : `${formatCount(metric.low)} to ${formatCount(metric.high)}`;
575
581
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
576
582
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size, children: [
577
583
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "~" }),
@@ -671,9 +677,9 @@ function rateLine(stage, index, entry, share, entryLabel) {
671
677
  if (index === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "100% \u2014 everyone who arrived" });
672
678
  if (stage.partial) {
673
679
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
674
- "only counted from ",
680
+ "tracking added ",
675
681
  stage.partial.from,
676
- ", so no share is shown"
682
+ ", so this step covers fewer days than the ones above \u2014 no share is shown"
677
683
  ] });
678
684
  }
679
685
  const canRate = entry >= MIN_RATE_DENOMINATOR && stage.value >= MIN_RATE_DENOMINATOR;
@@ -747,24 +753,24 @@ function FunnelChart({ stages, measurement }) {
747
753
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
748
754
  "div",
749
755
  {
750
- style: {
751
- ...barFill,
752
- width: `${width}%`,
753
- ...stage.partial ? {
754
- backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.fill")} 0 4px, transparent 4px 8px)`,
755
- backgroundColor: "transparent",
756
- outline: `1px solid ${mark("bar.fill")}`,
757
- outlineOffset: -1
758
- } : {}
759
- }
756
+ style: stage.partial ? {
757
+ height: "100%",
758
+ width: "100%",
759
+ borderRadius: 3,
760
+ backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.partial")} 0 4px, transparent 4px 8px)`
761
+ } : { ...barFill, width: `${width}%` }
760
762
  }
761
763
  ) }),
762
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: rateLine(stage, index, entry, share, stages[0]?.label ?? "") })
764
+ rateLine(stage, index, entry, share, stages[0]?.label ?? "")
763
765
  ]
764
766
  }
765
767
  )
766
768
  ] }, stage.key);
767
769
  }) }),
770
+ stages.some((stage) => stage.partial) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelKey, children: [
771
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: funnelKeySwatch }),
772
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "Striped: tracking started partway through this period, so the step covers fewer days than the ones above it and cannot be compared with them." })
773
+ ] }),
768
774
  withheldAny && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
769
775
  "Shares are shown only for steps at least ",
770
776
  MIN_RATE_DENOMINATOR,
@@ -781,6 +787,15 @@ function gapLabel(delta, measurement, entry = 0) {
781
787
  }
782
788
  return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
783
789
  }
790
+ var funnelKey = { display: "flex", alignItems: "center", gap: 8, paddingTop: 4 };
791
+ var funnelKeySwatch = {
792
+ display: "inline-block",
793
+ width: 24,
794
+ height: 10,
795
+ flex: "0 0 auto",
796
+ borderRadius: 3,
797
+ backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.partial")} 0 4px, transparent 4px 8px)`
798
+ };
784
799
  var funnelList = { listStyle: "none", margin: 0, padding: 0 };
785
800
  var funnelItem = { display: "grid", gap: 4 };
786
801
  function funnelGapStyle(hasBar) {
@@ -1008,6 +1023,7 @@ function EstimateDotPlot({
1008
1023
  function SurvivalLines({
1009
1024
  segments,
1010
1025
  stepLabels,
1026
+ partialSteps = [],
1011
1027
  minDenominator = MIN_RATE_DENOMINATOR
1012
1028
  }) {
1013
1029
  const [active, setActive] = import_react2.default.useState(null);
@@ -1026,6 +1042,7 @@ function SurvivalLines({
1026
1042
  const previous = i === 0 ? entry : segment.values[i - 1];
1027
1043
  if (value === null || value === void 0) break;
1028
1044
  if (i > 0 && (previous === null || previous === void 0 || previous < minDenominator)) break;
1045
+ if (partialSteps.includes(i)) break;
1029
1046
  const share = Math.min(1, value / entry);
1030
1047
  points.push([x(i), height - share * height]);
1031
1048
  shares.push(share);
@@ -1068,7 +1085,10 @@ function SurvivalLines({
1068
1085
  width: width / (stepLabels.length - 1) / (i === 0 || i === stepLabels.length - 1 ? 2 : 1),
1069
1086
  height,
1070
1087
  fill: "transparent",
1071
- onMouseEnter: () => setActive(i),
1088
+ onMouseEnter: () => {
1089
+ setSteppedByKey(false);
1090
+ setActive(i);
1091
+ },
1072
1092
  onMouseLeave: () => setActive(null)
1073
1093
  },
1074
1094
  label
@@ -1123,7 +1143,7 @@ function SurvivalLines({
1123
1143
  {
1124
1144
  type: "button",
1125
1145
  style: survivalTick(i === active, i, stepLabels.length),
1126
- "aria-pressed": i === active,
1146
+ "aria-label": `Read ${label}`,
1127
1147
  onMouseEnter: () => {
1128
1148
  setSteppedByKey(false);
1129
1149
  setActive(i);
@@ -2235,7 +2255,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
2235
2255
  markers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2236
2256
  LegendKey,
2237
2257
  {
2238
- swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendMarker }),
2258
+ swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendMarker, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendMarkerDot }) }),
2239
2259
  label: "Dashed rules with a dot mark campaign sends"
2240
2260
  }
2241
2261
  ),
@@ -2273,11 +2293,23 @@ function legendBlock(fill, edge) {
2273
2293
  var legendMarker = {
2274
2294
  display: "inline-block",
2275
2295
  position: "relative",
2276
- width: 0,
2296
+ width: 1,
2277
2297
  height: 12,
2278
2298
  borderLeft: "1px dashed currentColor",
2279
- opacity: 0.7,
2280
- boxShadow: "-2px -6px 0 0 currentColor, -1px -6px 0 0 currentColor"
2299
+ // The rule's OWN weight. The chart draws the line at 0.5 and only the dot at 0.7, so putting 0.7
2300
+ // on the whole element made the legend's rule heavier than the rule it names — the same class of
2301
+ // mismatch this legend work exists to remove.
2302
+ opacity: 0.5
2303
+ };
2304
+ var legendMarkerDot = {
2305
+ position: "absolute",
2306
+ top: -3,
2307
+ left: -2.5,
2308
+ width: 6,
2309
+ height: 6,
2310
+ borderRadius: "50%",
2311
+ background: "currentColor",
2312
+ opacity: 0.7
2281
2313
  };
2282
2314
  function shortfallKey(series) {
2283
2315
  const row = series.find((r) => r.shortfall);
@@ -2352,7 +2384,7 @@ function metricOr(metric, detail) {
2352
2384
  }
2353
2385
  var OLDER_ROUTE = "This site\u2019s analytics route is older than this figure \u2014 it will appear after the site is next deployed.";
2354
2386
  function maxOf(metrics) {
2355
- return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
2387
+ return metrics.reduce((max, metric) => !metric || metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
2356
2388
  }
2357
2389
  function finiteOrNull(value) {
2358
2390
  return typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -2395,7 +2427,15 @@ function OverviewPanel({ data, previous, onBrush }) {
2395
2427
  /* @__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: [
2396
2428
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Revenue" }),
2397
2429
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2398
- metricOr(data.revenue, OLDER_ROUTE).status === "ok" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatMoney(data.revenue.value, data.currency ?? null) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.revenue, OLDER_ROUTE), label: "Revenue" }),
2430
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2431
+ MetricFigure,
2432
+ {
2433
+ metric: metricOr(data.revenue, OLDER_ROUTE),
2434
+ label: "Revenue",
2435
+ unit: "money",
2436
+ currency: data.currency ?? null
2437
+ }
2438
+ ),
2399
2439
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delta, { current: metricSortValue(data.revenue), previous: metricSortValue(previous?.revenue) })
2400
2440
  ] }),
2401
2441
  rankLine(data.crossSource, (d) => d.revenue) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: rankLine(data.crossSource, (d) => d.revenue) })
@@ -2403,7 +2443,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2403
2443
  /* @__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: [
2404
2444
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Orders" }),
2405
2445
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2406
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.orders, label: "Orders" }),
2446
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.orders, OLDER_ROUTE), label: "Orders" }),
2407
2447
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delta, { current: metricSortValue(data.orders), previous: metricSortValue(previous?.orders) })
2408
2448
  ] }),
2409
2449
  rankLine(data.crossSource, (d) => d.orders) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: rankLine(data.crossSource, (d) => d.orders) })
@@ -2414,7 +2454,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2414
2454
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelPageviews, OLDER_ROUTE), label: "Pageviews" }),
2415
2455
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delta, { current: metricSortValue(data.vercelPageviews), previous: metricSortValue(previous?.vercelPageviews) })
2416
2456
  ] }),
2417
- data.vercelPageviews?.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Pageviews, from Vercel\u2019s own counter." })
2457
+ data.vercelPageviews && data.vercelPageviews.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Pageviews, from Vercel\u2019s own counter." })
2418
2458
  ] }) }),
2419
2459
  /* @__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: [
2420
2460
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Mailing list, total" }),
@@ -2433,7 +2473,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2433
2473
  ] });
2434
2474
  })()
2435
2475
  ] }),
2436
- data.audience?.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Everyone subscribed today, not just this period. Mailchimp\u2019s own count, so it is not consent-gated or blockable." })
2476
+ data.audience && data.audience.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Everyone subscribed today, not just this period. Mailchimp\u2019s own count, so it is not consent-gated or blockable." })
2437
2477
  ] }) })
2438
2478
  ] }),
2439
2479
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: cardGrid, children: [
@@ -2452,7 +2492,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2452
2492
  }
2453
2493
  ),
2454
2494
  averageOrderValue(data).status === "partial" && averageOrderValue(data).note && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: averageOrderValue(data).note }),
2455
- averageOrderValue(data).status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "From your orders, so exact." })
2495
+ averageOrderValue(data).status === "ok" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "From your orders, so exact." })
2456
2496
  ] }) }),
2457
2497
  /* @__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: [
2458
2498
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Visitors per order" }),
@@ -2468,7 +2508,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2468
2508
  unavailable: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: visitorsPerOrder(data), label: "Visitors per order" })
2469
2509
  }
2470
2510
  ),
2471
- data.vercelVisitors?.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s visitor count. A ceiling \u2014 some are bots or the same person twice." })
2511
+ data.vercelVisitors && data.vercelVisitors.status !== "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s visitor count. A ceiling \u2014 some are bots or the same person twice." })
2472
2512
  ] }) })
2473
2513
  ] }),
2474
2514
  (data.crossSource?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
@@ -2783,8 +2823,7 @@ function Verdict({ data, previous }) {
2783
2823
  const shortfall = data.shortfallRatio;
2784
2824
  const seeing = shortfall !== null && shortfall !== void 0 ? `GA4 is seeing ${formatPercent(1 - shortfall, 0)} of your traffic` : null;
2785
2825
  const broken = shortfall === null || shortfall === void 0 ? data.capture?.discrepancy ? "measurement disagrees between sources" : null : shortfall > 0.6 ? `${seeing} \u2014 treat its figures as broken` : shortfall > 0.35 ? `${seeing} \u2014 its figures are undercounts, not what happened` : data.capture?.discrepancy ? "measurement disagrees between sources" : null;
2786
- if (broken) parts.push(broken);
2787
- if (parts.length === 0) {
2826
+ if (parts.length === 0 && !broken) {
2788
2827
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, style: { borderLeftWidth: 3, borderLeftStyle: "solid" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: "No figures arrived for this window." }) });
2789
2828
  }
2790
2829
  const noticeable = shortfall !== null && shortfall !== void 0 && shortfall > 0.2;
@@ -2797,9 +2836,12 @@ function Verdict({ data, previous }) {
2797
2836
  tone: broken ? "caution" : "transparent",
2798
2837
  border: true,
2799
2838
  style: broken ? { borderLeftWidth: 3, borderLeftStyle: "solid" } : void 0,
2800
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 3, weight: broken ? "semibold" : "medium", children: [
2801
- parts.join(" \xB7 "),
2802
- closing
2839
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
2840
+ parts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 3, weight: broken ? "semibold" : "medium", children: [
2841
+ parts.join(" \xB7 "),
2842
+ closing
2843
+ ] }),
2844
+ broken && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: broken })
2803
2845
  ] })
2804
2846
  }
2805
2847
  );
@@ -2854,11 +2896,11 @@ function DataHealthPanel({ data, diagnostics }) {
2854
2896
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2855
2897
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2856
2898
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Pageviews Vercel counted" }),
2857
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.vercelPageviews, label: "Vercel pageviews", size: 3 })
2899
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelPageviews, OLDER_ROUTE), label: "Vercel pageviews", size: 3 })
2858
2900
  ] }),
2859
2901
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2860
2902
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Seen by Google Analytics" }),
2861
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Pageviews, label: "GA4 pageviews", size: 3 })
2903
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.ga4Pageviews, OLDER_ROUTE), label: "GA4 pageviews", size: 3 })
2862
2904
  ] }),
2863
2905
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Only one source answered for this range, so there is nothing to compare." })
2864
2906
  ] })
@@ -2895,7 +2937,7 @@ function DataHealthPanel({ data, diagnostics }) {
2895
2937
  ] }) }),
2896
2938
  data.estimatedSessions && data.estimatedSessions.status === "estimated" && /* @__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: [
2897
2939
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Sessions, corrected for what GA4 misses" }),
2898
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.estimatedSessions, label: "Estimated sessions" })
2940
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.estimatedSessions, OLDER_ROUTE), label: "Estimated sessions" })
2899
2941
  ] }) })
2900
2942
  ] }),
2901
2943
  Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
@@ -2929,11 +2971,11 @@ function DataHealthPanel({ data, diagnostics }) {
2929
2971
  ] }) }),
2930
2972
  /* @__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: [
2931
2973
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "GA4 sessions" }),
2932
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Sessions, label: "GA4 sessions" })
2974
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.ga4Sessions, OLDER_ROUTE), label: "GA4 sessions" })
2933
2975
  ] }) }),
2934
2976
  /* @__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: [
2935
2977
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Consent granted" }),
2936
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, share of visitors GA4 saw", unit: "percent" })
2978
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.consentRate, OLDER_ROUTE), label: "Consent granted, share of visitors GA4 saw", unit: "percent" })
2937
2979
  ] }) })
2938
2980
  ] })
2939
2981
  }
@@ -3187,6 +3229,10 @@ function JourneyPanel({ data }) {
3187
3229
  ...step.count.status === "partial" && step.count.coveredFrom ? { partial: { from: formatDay(step.count.coveredFrom) } } : {}
3188
3230
  }]
3189
3231
  );
3232
+ const wholeSpine = spine.filter((step) => {
3233
+ const cell = allSteps.find((s) => s.key === step.key);
3234
+ return cell?.count.status !== "partial";
3235
+ });
3190
3236
  const tracked = data.measurement === "sequence";
3191
3237
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
3192
3238
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: tracked ? "transparent" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
@@ -3199,12 +3245,12 @@ function JourneyPanel({ data }) {
3199
3245
  segments: segments.map((segment) => ({
3200
3246
  key: segment.key,
3201
3247
  label: segment.label,
3202
- values: spine.map((step) => {
3248
+ values: wholeSpine.map((step) => {
3203
3249
  const cell = segment.steps.find((s) => s.key === step.key);
3204
3250
  return cell ? metricSortValue(cell.count) : null;
3205
3251
  })
3206
3252
  })),
3207
- stepLabels: spine.map((step) => step.label)
3253
+ stepLabels: wholeSpine.map((step) => step.label)
3208
3254
  }
3209
3255
  ),
3210
3256
  segments.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(