@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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +100 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +100 -8
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-BUZuS1c-.d.mts → ranges-_SBvGBJx.d.mts} +20 -0
- package/dist/{ranges-BUZuS1c-.d.ts → ranges-_SBvGBJx.d.ts} +20 -0
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +109 -75
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +109 -75
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/reportData.ts +20 -0
- package/src/server/createHandler.ts +12 -3
- package/src/server/reports/acquisition.ts +47 -2
- package/src/studio/panels.test.tsx +83 -0
- package/src/studio/panels.tsx +134 -10
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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: "
|
|
2250
|
+
label: "Sells vs catalogue",
|
|
2173
2251
|
numeric: true,
|
|
2174
2252
|
sortValue: (row) => finiteOrNull(row.buyRate),
|
|
2175
2253
|
exportValue: (row) => {
|
|
2176
|
-
const
|
|
2177
|
-
return
|
|
2254
|
+
const index = buyRateIndex(row, medianBuyRate);
|
|
2255
|
+
return index === null ? null : `${index.toFixed(1)}x`;
|
|
2178
2256
|
},
|
|
2179
|
-
render: (row) =>
|
|
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",
|