@liiift-studio/sanity-visitor-insights 0.26.0 → 0.27.1

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-BUZuS1c-.mjs';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, 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-BUZuS1c-.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-_SBvGBJx.mjs';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, 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-_SBvGBJx.mjs';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
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-BUZuS1c-.js';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, 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-BUZuS1c-.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-_SBvGBJx.js';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, 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-_SBvGBJx.js';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
package/dist/index.js CHANGED
@@ -1595,7 +1595,7 @@ function OverviewPanel({ data, previous, onBrush }) {
1595
1595
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Pageviews, counted server-side." })
1596
1596
  ] }) }),
1597
1597
  /* @__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: [
1598
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Mailing list" }),
1598
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Mailing list, total" }),
1599
1599
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
1600
1600
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.audience, OLDER_ROUTE), label: "Mailing list members" }),
1601
1601
  (() => {
@@ -1608,7 +1608,7 @@ function OverviewPanel({ data, previous, onBrush }) {
1608
1608
  ] });
1609
1609
  })()
1610
1610
  ] }),
1611
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Mailchimp\u2019s own count \u2014 not consent-gated or blockable." })
1611
+ /* @__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." })
1612
1612
  ] }) })
1613
1613
  ] }),
1614
1614
  (data.crossSource?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
@@ -2017,10 +2017,59 @@ function AcquisitionPanel({ data, previous }) {
2017
2017
  // The quality signal. Ranked by volume alone, 27 Display sessions looked
2018
2018
  // equal to 27 from Search, which flatters the worst line of spend.
2019
2019
  render: (row) => finiteOrNull(row.engagementRate) === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatPercent(row.engagementRate, 0) })
2020
+ },
2021
+ {
2022
+ /*
2023
+ * What the channel BROUGHT, which is the question a foundry opens this tool
2024
+ * with and the one it could not answer. Sessions and engagement rank
2025
+ * channels by attention; nothing ranked them by money, so "is the design
2026
+ * press worth chasing" and "did the newsletter sell anything" were
2027
+ * unaskable while revenue sat one tab away, split by typeface and by
2028
+ * licence — the two cuts already available from the order list.
2029
+ *
2030
+ * Sorted on the SHARE, so the ordering holds even when Sanity supplies no
2031
+ * total and the money column is empty.
2032
+ */
2033
+ key: "brought",
2034
+ label: "Revenue (est.)",
2035
+ numeric: true,
2036
+ sortValue: (row) => finiteOrNull(row.revenueShare),
2037
+ exportValue: (row) => {
2038
+ const money = finiteOrNull(row.apportionedRevenue);
2039
+ return money === null ? null : formatMoney(money, data.currency ?? null);
2040
+ },
2041
+ render: (row) => {
2042
+ const share = finiteOrNull(row.revenueShare);
2043
+ if (share === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" });
2044
+ const money = finiteOrNull(row.apportionedRevenue);
2045
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 1, children: [
2046
+ money !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatMoney(money, data.currency ?? null) }),
2047
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2048
+ formatPercent(share, 0),
2049
+ " of tracked sales"
2050
+ ] })
2051
+ ] });
2052
+ }
2020
2053
  }
2021
2054
  ]
2022
2055
  }
2023
- )
2056
+ ),
2057
+ (data.trackedPurchases ?? 0) > 0 && finiteOrNull(data.actualRevenue) !== null && // Says exactly what was combined and what was estimated. The money in that column
2058
+ // is Sanity's real total spread across GA4's split — right in scale, and derived,
2059
+ // which is a different claim from measured. Saying so here is what makes the
2060
+ // column usable rather than another figure to distrust.
2061
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2062
+ "Revenue is ",
2063
+ formatMoney(finiteOrNull(data.actualRevenue) ?? 0, data.currency ?? null),
2064
+ " from",
2065
+ " ",
2066
+ finiteOrNull(data.actualOrders) === null ? "your orders" : `${formatCount(data.actualOrders)} orders`,
2067
+ " in Sanity, split across the ",
2068
+ formatCount(data.trackedPurchases),
2069
+ " purchase",
2070
+ data.trackedPurchases === 1 ? "" : "s",
2071
+ " Google Analytics attributed to a source. Google Analytics sees a fraction of your traffic, but it loses it fairly evenly across channels \u2014 so the split is far more trustworthy than its own totals. Treat these as proportions with a real total behind them, not as measured takings per channel."
2072
+ ] })
2024
2073
  ] }),
2025
2074
  data.rowsWithheld && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NoticeList, { notices: ["GA4 withheld some low-traffic rows for privacy, so this list is shorter than reality."] })
2026
2075
  ] });
@@ -2102,12 +2151,24 @@ function JourneyPanel({ data }) {
2102
2151
  ] })
2103
2152
  ] });
2104
2153
  }
2154
+ function medianOf(values) {
2155
+ const usable = values.filter((v) => v !== null && Number.isFinite(v) && v > 0).sort((a, b) => a - b);
2156
+ if (usable.length < 3) return null;
2157
+ const middle = Math.floor(usable.length / 2);
2158
+ return usable.length % 2 === 0 ? (usable[middle - 1] + usable[middle]) / 2 : usable[middle];
2159
+ }
2160
+ function buyRateIndex(row, median) {
2161
+ const rate = row.buyRate;
2162
+ if (median === null || median <= 0 || rate === null || rate === void 0 || !Number.isFinite(rate)) return null;
2163
+ return rate / median;
2164
+ }
2105
2165
  function TypefaceInterestPanel({ data }) {
2166
+ const medianBuyRate = medianOf((data.rows ?? []).map((row) => finiteOrNull(row.buyRate)));
2106
2167
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
2107
2168
  /* @__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.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
2108
2169
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2109
2170
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Engagement by typeface" }),
2110
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Sort by any column. Sort on buy rate to find families that are looked at and do not sell." }),
2171
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Sort by any column. \u201CSells vs catalogue\u201D compares each family\u2019s orders-per-view against the median family, which is the part of that ratio that holds even though Google Analytics only sees a fraction of the views. It ranks families against each other; it is not the share of visitors who buy, and at these order volumes a family with one or two sales will swing a long way." }),
2111
2172
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2112
2173
  SortableTable,
2113
2174
  {
@@ -2168,15 +2229,46 @@ function TypefaceInterestPanel({ data }) {
2168
2229
  }
2169
2230
  },
2170
2231
  {
2232
+ /*
2233
+ * Relative to the catalogue, NOT an absolute rate.
2234
+ *
2235
+ * This is Sanity orders over GA4 views: a complete numerator on a lossy
2236
+ * denominator. Printed as "0.85%" it invited the reading "eight in a
2237
+ * thousand visitors buy this", which is wrong by whatever multiple GA4 is
2238
+ * missing — and it MOVES when the tag breaks. When Darden's GA4 count fell
2239
+ * from 471 a day to 70, every family's printed rate multiplied by about
2240
+ * seven, and the families that looked best were the ones GA4 had stopped
2241
+ * seeing.
2242
+ *
2243
+ * What survives that is the ORDER. The loss is roughly uniform across
2244
+ * families, so it cancels in a comparison between them even as it wrecks
2245
+ * the absolute figure. Showing each family against the catalogue median
2246
+ * keeps the one question this column can answer — which families convert
2247
+ * better or worse than the rest — and drops the one it cannot.
2248
+ */
2171
2249
  key: "buyRate",
2172
- label: "Buy rate",
2250
+ label: "Sells vs catalogue",
2173
2251
  numeric: true,
2174
2252
  sortValue: (row) => finiteOrNull(row.buyRate),
2175
2253
  exportValue: (row) => {
2176
- const r = finiteOrNull(row.buyRate);
2177
- return r === null ? null : formatPercent(r, 2);
2254
+ const index = buyRateIndex(row, medianBuyRate);
2255
+ return index === null ? null : `${index.toFixed(1)}x`;
2178
2256
  },
2179
- render: (row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, "aria-label": finiteOrNull(row.buyRate) === null ? `${row.typeface} buy rate unavailable` : void 0, children: finiteOrNull(row.buyRate) === null ? "\u2014" : formatPercent(row.buyRate, 2) })
2257
+ render: (row) => {
2258
+ const index = buyRateIndex(row, medianBuyRate);
2259
+ if (index === null) {
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, "aria-label": `${row.typeface} not comparable`, children: "\u2014" });
2261
+ }
2262
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, children: [
2263
+ index.toFixed(1),
2264
+ "\xD7",
2265
+ " ",
2266
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { opacity: 0.6, fontSize: "0.85em" }, children: [
2267
+ index >= 1.15 ? "above" : index <= 0.85 ? "below" : "at",
2268
+ " median"
2269
+ ] })
2270
+ ] });
2271
+ }
2180
2272
  },
2181
2273
  {
2182
2274
  key: "testRate",