@liiift-studio/sanity-visitor-insights 0.27.1 → 0.28.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 +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +61 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -41
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-_SBvGBJx.d.mts → ranges-D1A9JY7h.d.mts} +33 -9
- package/dist/{ranges-_SBvGBJx.d.ts → ranges-D1A9JY7h.d.ts} +33 -9
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +96 -23
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +96 -23
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/reportData.ts +33 -9
- package/src/server/reports/acquisition.ts +161 -16
- package/src/server/reports/measurementHealth.ts +41 -10
- package/src/server/reports/reports.test.ts +5 -1
- package/src/studio/CrossSourceTimeline.tsx +18 -4
- package/src/studio/Figure.tsx +19 -4
- package/src/studio/VisitorInsightsTool.tsx +3 -3
- package/src/studio/panels.test.tsx +126 -20
- package/src/studio/panels.tsx +83 -39
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-D1A9JY7h.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-D1A9JY7h.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -117,7 +117,7 @@ declare function formatCount(value: number): string;
|
|
|
117
117
|
declare function formatPercent(ratio: number, digits?: number): string;
|
|
118
118
|
/** Props for MetricFigure. */
|
|
119
119
|
/** Format a money value in the site's currency, falling back to a plain number. */
|
|
120
|
-
declare function formatMoney(value: number, currency: string | null): string;
|
|
120
|
+
declare function formatMoney(value: number, currency: string | null, fractionDigits?: 0 | 2): string;
|
|
121
121
|
/** Props for Delta. */
|
|
122
122
|
interface DeltaProps {
|
|
123
123
|
/** This period's value, or null when it could not be measured. */
|
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-D1A9JY7h.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-D1A9JY7h.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -117,7 +117,7 @@ declare function formatCount(value: number): string;
|
|
|
117
117
|
declare function formatPercent(ratio: number, digits?: number): string;
|
|
118
118
|
/** Props for MetricFigure. */
|
|
119
119
|
/** Format a money value in the site's currency, falling back to a plain number. */
|
|
120
|
-
declare function formatMoney(value: number, currency: string | null): string;
|
|
120
|
+
declare function formatMoney(value: number, currency: string | null, fractionDigits?: 0 | 2): string;
|
|
121
121
|
/** Props for Delta. */
|
|
122
122
|
interface DeltaProps {
|
|
123
123
|
/** This period's value, or null when it could not be measured. */
|
package/dist/index.js
CHANGED
|
@@ -219,19 +219,25 @@ var REASON_TEXT = {
|
|
|
219
219
|
source_error: "Source did not respond",
|
|
220
220
|
not_applicable: "Does not apply to this site"
|
|
221
221
|
};
|
|
222
|
+
var LOCALE = "en-GB";
|
|
222
223
|
function formatCount(value) {
|
|
223
|
-
return new Intl.NumberFormat(
|
|
224
|
+
return new Intl.NumberFormat(LOCALE).format(Math.round(value));
|
|
224
225
|
}
|
|
225
226
|
function formatPercent(ratio, digits = 0) {
|
|
226
227
|
return `${(ratio * 100).toFixed(digits)}%`;
|
|
227
228
|
}
|
|
228
|
-
function formatMoney(value, currency) {
|
|
229
|
+
function formatMoney(value, currency, fractionDigits = 0) {
|
|
229
230
|
if (!currency) return formatCount(Math.round(value));
|
|
230
231
|
try {
|
|
231
|
-
return new Intl.NumberFormat(
|
|
232
|
+
return new Intl.NumberFormat(LOCALE, {
|
|
232
233
|
style: "currency",
|
|
233
234
|
currency,
|
|
234
|
-
|
|
235
|
+
// One precision for the whole column, chosen by the caller, not by each value's own size.
|
|
236
|
+
// Switching at 1,000 put "$850.00" and "$1,200" in adjacent rows, and printed an axis
|
|
237
|
+
// reading "$1,000" over a baseline reading "$0.00" — the mismatch the timeline's own
|
|
238
|
+
// comment claims to have fixed by routing both ends through this function.
|
|
239
|
+
maximumFractionDigits: fractionDigits,
|
|
240
|
+
minimumFractionDigits: fractionDigits
|
|
235
241
|
}).format(value);
|
|
236
242
|
} catch {
|
|
237
243
|
return `${formatCount(Math.round(value))} ${currency}`;
|
|
@@ -1203,7 +1209,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1203
1209
|
}
|
|
1204
1210
|
const sentences = [];
|
|
1205
1211
|
if (excess > 0) {
|
|
1206
|
-
sentences.push(`${row.shortfall.source} counted ${formatValue(Math.round(excess), row.unit, currency)}
|
|
1212
|
+
sentences.push(`${row.shortfall.source} counted ${formatValue(Math.round(excess), row.unit, currency)} more ${row.label.toLowerCase()} than ${row.source} on some days, which usually means a tag firing twice rather than extra traffic.`);
|
|
1207
1213
|
}
|
|
1208
1214
|
if (gap <= 0) return sentences;
|
|
1209
1215
|
const total = `${row.shortfall.source} missed ${formatValue(Math.round(gap), row.unit, currency)} ${row.label.toLowerCase()} over this period`;
|
|
@@ -1222,7 +1228,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1222
1228
|
setHoverIndex(null);
|
|
1223
1229
|
setBrushAnchor(null);
|
|
1224
1230
|
setBrushed(null);
|
|
1225
|
-
setTooShort(false);
|
|
1226
1231
|
},
|
|
1227
1232
|
onPointerDown: (event) => {
|
|
1228
1233
|
if (!onBrush) return;
|
|
@@ -1274,7 +1279,8 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1274
1279
|
event.preventDefault();
|
|
1275
1280
|
setHoverIndex(next);
|
|
1276
1281
|
if (event.shiftKey && onBrush) {
|
|
1277
|
-
const anchor =
|
|
1282
|
+
const anchor = brushAnchor ?? current;
|
|
1283
|
+
if (brushAnchor === null) setBrushAnchor(current);
|
|
1278
1284
|
setBrushed([Math.min(anchor, next), Math.max(anchor, next)]);
|
|
1279
1285
|
}
|
|
1280
1286
|
},
|
|
@@ -1296,7 +1302,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1296
1302
|
const pitch = plotWidth / Math.max(1, dates.length - 1);
|
|
1297
1303
|
const stemWidth = Math.max(1, Math.min(9, pitch > 2 ? pitch - 1 : pitch));
|
|
1298
1304
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
1299
|
-
const zeroes = row.mark === "events" ? row.points.filter((p) => p.value === 0) : [];
|
|
1305
|
+
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
1300
1306
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1301
1307
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints) ?? "" : "";
|
|
1302
1308
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -1672,7 +1678,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1672
1678
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1673
1679
|
ChartData,
|
|
1674
1680
|
{
|
|
1675
|
-
note: "A negative miss means GA4 counted
|
|
1681
|
+
note: "A negative miss means GA4 counted more than Vercel that day \u2014 usually a tag firing twice, not extra traffic.",
|
|
1676
1682
|
label: "Show these figures as a table",
|
|
1677
1683
|
rows: data.crossSource ?? [],
|
|
1678
1684
|
rowKey: (d) => d.date,
|
|
@@ -2031,7 +2037,9 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2031
2037
|
* total and the money column is empty.
|
|
2032
2038
|
*/
|
|
2033
2039
|
key: "brought",
|
|
2034
|
-
|
|
2040
|
+
// The hedge belongs on the SPLIT, not on the money: the total is exact and the
|
|
2041
|
+
// division of it is the estimate. "(est.)" on a revenue header implied the reverse.
|
|
2042
|
+
label: "Revenue, split",
|
|
2035
2043
|
numeric: true,
|
|
2036
2044
|
sortValue: (row) => finiteOrNull(row.revenueShare),
|
|
2037
2045
|
exportValue: (row) => {
|
|
@@ -2040,7 +2048,7 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2040
2048
|
},
|
|
2041
2049
|
render: (row) => {
|
|
2042
2050
|
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" });
|
|
2051
|
+
if (share === null || share === 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" });
|
|
2044
2052
|
const money = finiteOrNull(row.apportionedRevenue);
|
|
2045
2053
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 1, children: [
|
|
2046
2054
|
money !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatMoney(money, data.currency ?? null) }),
|
|
@@ -2054,7 +2062,7 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2054
2062
|
]
|
|
2055
2063
|
}
|
|
2056
2064
|
),
|
|
2057
|
-
|
|
2065
|
+
data.splitIsSound === true && finiteOrNull(data.actualRevenue) !== null && // Says exactly what was combined and what was estimated. The money in that column
|
|
2058
2066
|
// is Sanity's real total spread across GA4's split — right in scale, and derived,
|
|
2059
2067
|
// which is a different claim from measured. Saying so here is what makes the
|
|
2060
2068
|
// column usable rather than another figure to distrust.
|
|
@@ -2065,10 +2073,16 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2065
2073
|
" ",
|
|
2066
2074
|
finiteOrNull(data.actualOrders) === null ? "your orders" : `${formatCount(data.actualOrders)} orders`,
|
|
2067
2075
|
" in Sanity, split across the ",
|
|
2068
|
-
formatCount(data.trackedPurchases),
|
|
2076
|
+
formatCount(data.trackedPurchases ?? 0),
|
|
2069
2077
|
" purchase",
|
|
2070
2078
|
data.trackedPurchases === 1 ? "" : "s",
|
|
2071
|
-
" Google Analytics attributed to a source. Google Analytics sees a fraction of your traffic,
|
|
2079
|
+
" Google Analytics attributed to a source. Google Analytics sees a fraction of your traffic, so treat this as a proportion with a real total behind it rather than measured takings per channel. Two things to hold in mind: it credits whichever source a buyer arrived from on the visit they bought, which for a considered purchase is often a bookmark rather than the blog that first sent them; and its loss is not perfectly even across channels \u2014 ad-blocking runs high among designers, so earned referrals and direct visits are likelier to be under-credited than paid clicks.",
|
|
2080
|
+
finiteOrNull(data.shownPurchases) !== null && (data.shownPurchases ?? 0) < (data.trackedPurchases ?? 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
2081
|
+
" ",
|
|
2082
|
+
"The rows above account for ",
|
|
2083
|
+
formatCount(data.shownPurchases ?? 0),
|
|
2084
|
+
" of those sales; the rest came from sources outside this list, so this column does not add up to the whole."
|
|
2085
|
+
] })
|
|
2072
2086
|
] })
|
|
2073
2087
|
] }),
|
|
2074
2088
|
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."] })
|
|
@@ -2151,24 +2165,31 @@ function JourneyPanel({ data }) {
|
|
|
2151
2165
|
] })
|
|
2152
2166
|
] });
|
|
2153
2167
|
}
|
|
2154
|
-
function
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2168
|
+
function catalogueRate(rows) {
|
|
2169
|
+
let views = 0;
|
|
2170
|
+
let sales = 0;
|
|
2171
|
+
for (const row of rows) {
|
|
2172
|
+
const viewed = metricSortValue(row.viewed);
|
|
2173
|
+
const bought = metricSortValue(row.bought);
|
|
2174
|
+
if (viewed === null || bought === null) continue;
|
|
2175
|
+
views += viewed;
|
|
2176
|
+
sales += bought;
|
|
2177
|
+
}
|
|
2178
|
+
return views > 0 && sales > 0 ? sales / views : null;
|
|
2159
2179
|
}
|
|
2160
|
-
function buyRateIndex(row,
|
|
2180
|
+
function buyRateIndex(row, benchmark) {
|
|
2161
2181
|
const rate = row.buyRate;
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2182
|
+
if (benchmark === null || benchmark <= 0) return null;
|
|
2183
|
+
if (rate === null || rate === void 0 || !Number.isFinite(rate) || rate < 0) return null;
|
|
2184
|
+
return rate / benchmark;
|
|
2164
2185
|
}
|
|
2165
2186
|
function TypefaceInterestPanel({ data }) {
|
|
2166
|
-
const
|
|
2187
|
+
const benchmark = catalogueRate(data.rows ?? []);
|
|
2167
2188
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
2168
2189
|
/* @__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 }) }),
|
|
2169
2190
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2170
2191
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Engagement by typeface" }),
|
|
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
|
|
2192
|
+
/* @__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 sales-per-view against the catalogue as a whole \u2014 1.0\xD7 is average, and sorting up finds the families that are looked at and do not sell. It is a comparison between your families, not the share of visitors who buy: Google Analytics sees only a fraction of the views, which moves every family together and so cancels out here. With one or two sales a family will still swing a long way, so read it alongside the order counts beside it." }),
|
|
2172
2193
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2173
2194
|
SortableTable,
|
|
2174
2195
|
{
|
|
@@ -2249,24 +2270,26 @@ function TypefaceInterestPanel({ data }) {
|
|
|
2249
2270
|
key: "buyRate",
|
|
2250
2271
|
label: "Sells vs catalogue",
|
|
2251
2272
|
numeric: true,
|
|
2252
|
-
|
|
2273
|
+
// The INDEX, so a row rendered as a dash cannot sort above a row with a value.
|
|
2274
|
+
// Sorting on the raw rate returned 0 — a real number — for a family whose
|
|
2275
|
+
// cell showed "—", so ascending put a block of dashes at the top of the
|
|
2276
|
+
// table, which is the one thing this file's header forbids.
|
|
2277
|
+
sortValue: (row) => buyRateIndex(row, benchmark),
|
|
2253
2278
|
exportValue: (row) => {
|
|
2254
|
-
const index = buyRateIndex(row,
|
|
2255
|
-
return index === null ? null : `${index.toFixed(1)}x`;
|
|
2279
|
+
const index = buyRateIndex(row, benchmark);
|
|
2280
|
+
return index === null ? null : `${index.toFixed(1)}x catalogue`;
|
|
2256
2281
|
},
|
|
2257
2282
|
render: (row) => {
|
|
2258
|
-
const index = buyRateIndex(row,
|
|
2283
|
+
const index = buyRateIndex(row, benchmark);
|
|
2259
2284
|
if (index === null) {
|
|
2260
2285
|
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
2286
|
}
|
|
2287
|
+
if (index === 0) {
|
|
2288
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: "No sales" });
|
|
2289
|
+
}
|
|
2262
2290
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, children: [
|
|
2263
2291
|
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
|
-
] })
|
|
2292
|
+
"\xD7 catalogue"
|
|
2270
2293
|
] });
|
|
2271
2294
|
}
|
|
2272
2295
|
},
|
|
@@ -2613,9 +2636,10 @@ function CoverageRibbon({ ratio }) {
|
|
|
2613
2636
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: ratio > 0.6 ? "critical" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2614
2637
|
"Every figure on this tab comes from Google Analytics, which is seeing about ",
|
|
2615
2638
|
seen,
|
|
2616
|
-
"% of this site\u2019s traffic in this window
|
|
2617
|
-
|
|
2618
|
-
|
|
2639
|
+
"% of this site\u2019s traffic in this window \u2014 so the real numbers are roughly",
|
|
2640
|
+
" ",
|
|
2641
|
+
seen > 0 ? `${(100 / seen).toFixed(1)}\xD7` : "many times",
|
|
2642
|
+
" these. Data health explains why."
|
|
2619
2643
|
] }) });
|
|
2620
2644
|
}
|
|
2621
2645
|
function ReadyReport({
|