@liiift-studio/sanity-visitor-insights 0.51.0 → 0.52.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/README.md +1 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +99 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-CeB8esIA.d.mts → ranges-DBbZ0tg7.d.mts} +30 -1
- package/dist/{ranges-CeB8esIA.d.ts → ranges-DBbZ0tg7.d.ts} +30 -1
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +60 -6
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +60 -6
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-CGDmz9Bt.d.mts → vercel-pqyL1I1o.d.mts} +18 -1
- package/dist/{vercel-CGDmz9Bt.d.ts → vercel-pqyL1I1o.d.ts} +18 -1
- package/package.json +1 -1
- package/src/reportData.ts +30 -0
- package/src/server/ga4.ts +44 -6
- package/src/server/reports/journey.ts +59 -1
- package/src/server/reports/reports.test.ts +6 -5
- package/src/studio/panels.test.tsx +55 -0
- package/src/studio/panels.tsx +136 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ only as a conversion anchor and a count; no revenue, and no customer data.
|
|
|
15
15
|
|---|---|
|
|
16
16
|
| **Measurement health** | How much of reality does each source actually see? |
|
|
17
17
|
| **Acquisition** | Where do visitors come from, and how many are design-industry referrals? |
|
|
18
|
-
| **Journey** | How far do visitors get,
|
|
18
|
+
| **Journey** | How far do visitors get, where do they stop, and is it different on mobile? |
|
|
19
19
|
| **Typeface interest** | Which families get viewed, tested and bought? |
|
|
20
20
|
| **Diagnostics** | What to fix before trusting any of the above? |
|
|
21
21
|
|
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,
|
|
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-DBbZ0tg7.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 JourneySegment, o as JourneyStep, L as LandingPage, p as LicenceTierRow, P as PREEXISTING, q as REPORT_NAMES, r as ReportError, S as SiteAnalyticsConfig, s as SourceName, t as SourceRow, u as SourceStatus, v as TimelineEvent, w as TypefaceInterestRow, U as UnavailableReason, x as coverageForRange, y as estimated, z as isReportName, B as ok, F as partial, G as previousRange, H as resolveRange, I as unavailable, K as validateSiteConfig, N as valueOrNull } from './ranges-DBbZ0tg7.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -606,7 +606,6 @@ declare function AcquisitionPanel({ data, previous }: {
|
|
|
606
606
|
data: AcquisitionData;
|
|
607
607
|
previous?: AcquisitionData;
|
|
608
608
|
}): React.ReactElement;
|
|
609
|
-
/** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
|
|
610
609
|
declare function JourneyPanel({ data }: {
|
|
611
610
|
data: JourneyData;
|
|
612
611
|
}): React.ReactElement;
|
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,
|
|
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-DBbZ0tg7.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 JourneySegment, o as JourneyStep, L as LandingPage, p as LicenceTierRow, P as PREEXISTING, q as REPORT_NAMES, r as ReportError, S as SiteAnalyticsConfig, s as SourceName, t as SourceRow, u as SourceStatus, v as TimelineEvent, w as TypefaceInterestRow, U as UnavailableReason, x as coverageForRange, y as estimated, z as isReportName, B as ok, F as partial, G as previousRange, H as resolveRange, I as unavailable, K as validateSiteConfig, N as valueOrNull } from './ranges-DBbZ0tg7.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -606,7 +606,6 @@ declare function AcquisitionPanel({ data, previous }: {
|
|
|
606
606
|
data: AcquisitionData;
|
|
607
607
|
previous?: AcquisitionData;
|
|
608
608
|
}): React.ReactElement;
|
|
609
|
-
/** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
|
|
610
609
|
declare function JourneyPanel({ data }: {
|
|
611
610
|
data: JourneyData;
|
|
612
611
|
}): React.ReactElement;
|
package/dist/index.js
CHANGED
|
@@ -75,7 +75,7 @@ var import_sanity2 = require("sanity");
|
|
|
75
75
|
var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
|
|
76
76
|
|
|
77
77
|
// src/studio/VisitorInsightsTool.tsx
|
|
78
|
-
var
|
|
78
|
+
var import_react5 = __toESM(require("react"));
|
|
79
79
|
var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
|
|
80
80
|
|
|
81
81
|
// src/studio/useReport.ts
|
|
@@ -1067,6 +1067,7 @@ var visuallyHidden = {
|
|
|
1067
1067
|
var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
|
|
1068
1068
|
|
|
1069
1069
|
// src/studio/panels.tsx
|
|
1070
|
+
var import_react4 = __toESM(require("react"));
|
|
1070
1071
|
var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
|
|
1071
1072
|
|
|
1072
1073
|
// src/studio/CrossSourceTimeline.tsx
|
|
@@ -1803,6 +1804,24 @@ var figureRow = {
|
|
|
1803
1804
|
gap: 10,
|
|
1804
1805
|
flexWrap: "wrap"
|
|
1805
1806
|
};
|
|
1807
|
+
var segmentRow = { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" };
|
|
1808
|
+
function segmentButton(selected) {
|
|
1809
|
+
return {
|
|
1810
|
+
appearance: "none",
|
|
1811
|
+
background: selected ? "var(--card-bg-color, rgba(128,128,128,0.12))" : "transparent",
|
|
1812
|
+
border: `1px solid ${selected ? "currentColor" : "var(--card-border-color, rgba(128,128,128,0.3))"}`,
|
|
1813
|
+
borderRadius: 3,
|
|
1814
|
+
color: "inherit",
|
|
1815
|
+
font: "inherit",
|
|
1816
|
+
fontSize: "0.85em",
|
|
1817
|
+
padding: "4px 10px",
|
|
1818
|
+
cursor: "pointer",
|
|
1819
|
+
display: "flex",
|
|
1820
|
+
alignItems: "baseline",
|
|
1821
|
+
gap: 7
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
var segmentRate = { opacity: 0.65, fontVariantNumeric: "tabular-nums" };
|
|
1806
1825
|
var sourceLink = {
|
|
1807
1826
|
color: "inherit",
|
|
1808
1827
|
textDecoration: "underline",
|
|
@@ -2455,8 +2474,33 @@ function AcquisitionPanel({ data, previous }) {
|
|
|
2455
2474
|
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."] })
|
|
2456
2475
|
] });
|
|
2457
2476
|
}
|
|
2477
|
+
function firstStepRate(steps) {
|
|
2478
|
+
const second = steps[1];
|
|
2479
|
+
if (!second) return null;
|
|
2480
|
+
const rate = second.conversionFromPrevious;
|
|
2481
|
+
return typeof rate === "number" && Number.isFinite(rate) ? rate : null;
|
|
2482
|
+
}
|
|
2483
|
+
function spread(segments) {
|
|
2484
|
+
const MIN_USERS = 200;
|
|
2485
|
+
const RATIO = 3;
|
|
2486
|
+
const rated = segments.map((segment) => ({
|
|
2487
|
+
label: segment.label,
|
|
2488
|
+
rate: firstStepRate(segment.steps),
|
|
2489
|
+
users: metricSortValue(segment.steps[0]?.count) ?? 0
|
|
2490
|
+
})).filter((row) => row.rate !== null && row.rate > 0 && row.users >= MIN_USERS);
|
|
2491
|
+
if (rated.length < 2) return null;
|
|
2492
|
+
const sorted = [...rated].sort((a, b) => b.rate - a.rate);
|
|
2493
|
+
const best = sorted[0];
|
|
2494
|
+
const worst = sorted[sorted.length - 1];
|
|
2495
|
+
const ratio = best.rate / worst.rate;
|
|
2496
|
+
if (ratio < RATIO) return null;
|
|
2497
|
+
return `${worst.label} reaches the second step at ${formatPercent(worst.rate, 2)} against ${best.label}'s ${formatPercent(best.rate, 2)} \u2014 about ${Math.round(ratio)} times worse, on ${formatCount(worst.users)} visitors. The combined funnel above averages the two and describes neither.`;
|
|
2498
|
+
}
|
|
2458
2499
|
function JourneyPanel({ data }) {
|
|
2459
|
-
const
|
|
2500
|
+
const [segmentKey, setSegmentKey] = import_react4.default.useState(null);
|
|
2501
|
+
const segments = data.segments ?? [];
|
|
2502
|
+
const segment = segmentKey === null ? null : segments.find((row) => row.key === segmentKey) ?? null;
|
|
2503
|
+
const allSteps = segment ? segment.steps : data.steps ?? [];
|
|
2460
2504
|
const shown = allSteps.filter((step) => step.count.status !== "unavailable");
|
|
2461
2505
|
const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
|
|
2462
2506
|
const stages = shown.flatMap(
|
|
@@ -2468,6 +2512,44 @@ function JourneyPanel({ data }) {
|
|
|
2468
2512
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
|
|
2469
2513
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
2470
2514
|
] }) }),
|
|
2515
|
+
segments.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
2516
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: segmentRow, role: "radiogroup", "aria-label": `Funnel by ${data.segmentDimension ?? "segment"}`, children: [
|
|
2517
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
2518
|
+
"button",
|
|
2519
|
+
{
|
|
2520
|
+
type: "button",
|
|
2521
|
+
role: "radio",
|
|
2522
|
+
"aria-checked": segmentKey === null,
|
|
2523
|
+
style: segmentButton(segmentKey === null),
|
|
2524
|
+
onClick: () => setSegmentKey(null),
|
|
2525
|
+
children: "Everyone"
|
|
2526
|
+
}
|
|
2527
|
+
),
|
|
2528
|
+
segments.map((row) => {
|
|
2529
|
+
const rate = firstStepRate(row.steps);
|
|
2530
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
2531
|
+
"button",
|
|
2532
|
+
{
|
|
2533
|
+
type: "button",
|
|
2534
|
+
role: "radio",
|
|
2535
|
+
"aria-checked": segmentKey === row.key,
|
|
2536
|
+
style: segmentButton(segmentKey === row.key),
|
|
2537
|
+
onClick: () => setSegmentKey(row.key),
|
|
2538
|
+
children: [
|
|
2539
|
+
row.label,
|
|
2540
|
+
rate !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: segmentRate, children: formatPercent(rate, 2) })
|
|
2541
|
+
]
|
|
2542
|
+
},
|
|
2543
|
+
row.key
|
|
2544
|
+
);
|
|
2545
|
+
})
|
|
2546
|
+
] }),
|
|
2547
|
+
spread(segments) && /* Said in words, once, where the funnel is. The whole reason to split a funnel
|
|
2548
|
+
is that the halves differ; when they differ by a multiple rather than a
|
|
2549
|
+
margin, the undivided funnel above describes nobody and the reader should
|
|
2550
|
+
be told rather than left to press the buttons and notice. */
|
|
2551
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) })
|
|
2552
|
+
] }),
|
|
2471
2553
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
|
|
2472
2554
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
|
|
2473
2555
|
(data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -2878,13 +2960,13 @@ function RangeSelector({
|
|
|
2878
2960
|
onChange,
|
|
2879
2961
|
onCustomChange
|
|
2880
2962
|
}) {
|
|
2881
|
-
const refs = (0,
|
|
2882
|
-
const [pickerOpen, setPickerOpen] = (0,
|
|
2883
|
-
const [draft, setDraft] = (0,
|
|
2884
|
-
|
|
2963
|
+
const refs = (0, import_react5.useRef)([]);
|
|
2964
|
+
const [pickerOpen, setPickerOpen] = (0, import_react5.useState)(value === "custom");
|
|
2965
|
+
const [draft, setDraft] = (0, import_react5.useState)(custom);
|
|
2966
|
+
import_react5.default.useEffect(() => {
|
|
2885
2967
|
setDraft(custom);
|
|
2886
2968
|
}, [custom.start, custom.end]);
|
|
2887
|
-
const onKeyDown = (0,
|
|
2969
|
+
const onKeyDown = (0, import_react5.useCallback)(
|
|
2888
2970
|
(event) => {
|
|
2889
2971
|
const currentIndex = RANGES.findIndex((r) => r.key === value);
|
|
2890
2972
|
let nextIndex = null;
|
|
@@ -3004,8 +3086,8 @@ function SourceStatusRow({ sources }) {
|
|
|
3004
3086
|
] }) });
|
|
3005
3087
|
}
|
|
3006
3088
|
function PanelTabs({ value, onChange }) {
|
|
3007
|
-
const refs = (0,
|
|
3008
|
-
const onKeyDown = (0,
|
|
3089
|
+
const refs = (0, import_react5.useRef)([]);
|
|
3090
|
+
const onKeyDown = (0, import_react5.useCallback)(
|
|
3009
3091
|
(event) => {
|
|
3010
3092
|
const currentIndex = PANELS.findIndex((p) => p.id === value);
|
|
3011
3093
|
let nextIndex = null;
|
|
@@ -3209,7 +3291,7 @@ function isoDaysAgo(days) {
|
|
|
3209
3291
|
return new Date(utcMidnight - days * 864e5).toISOString().slice(0, 10);
|
|
3210
3292
|
}
|
|
3211
3293
|
var COMPARED_TABS = ["overview", "acquisition"];
|
|
3212
|
-
var PanelBoundary = class extends
|
|
3294
|
+
var PanelBoundary = class extends import_react5.default.Component {
|
|
3213
3295
|
constructor(props) {
|
|
3214
3296
|
super(props);
|
|
3215
3297
|
this.state = { error: null, shownFor: props.resetKey };
|
|
@@ -3248,16 +3330,16 @@ var PanelBoundary = class extends import_react4.default.Component {
|
|
|
3248
3330
|
function VisitorInsightsTool(props) {
|
|
3249
3331
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
3250
3332
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
3251
|
-
const [linked] = (0,
|
|
3333
|
+
const [linked] = (0, import_react5.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
|
|
3252
3334
|
const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
|
|
3253
3335
|
const linkedTab = PANELS.some((p) => p.id === linked.tab) ? linked.tab : null;
|
|
3254
|
-
const [range, setRange] = (0,
|
|
3255
|
-
const [rangeBeforeBrush, setRangeBeforeBrush] = (0,
|
|
3256
|
-
const [custom, setCustom] = (0,
|
|
3336
|
+
const [range, setRange] = (0, import_react5.useState)(linkedRange ?? "month");
|
|
3337
|
+
const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react5.useState)(linkedRange ?? "month");
|
|
3338
|
+
const [custom, setCustom] = (0, import_react5.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
|
|
3257
3339
|
const atPresent = custom.end >= isoDaysAgo(0);
|
|
3258
|
-
const [compare, setCompare] = (0,
|
|
3259
|
-
const [activePanel, setActivePanel] = (0,
|
|
3260
|
-
|
|
3340
|
+
const [compare, setCompare] = (0, import_react5.useState)("previous-period");
|
|
3341
|
+
const [activePanel, setActivePanel] = (0, import_react5.useState)(linkedTab ?? "overview");
|
|
3342
|
+
import_react5.default.useEffect(() => {
|
|
3261
3343
|
if (typeof window === "undefined") return;
|
|
3262
3344
|
const next = mergeIntoHash(window.location.hash, {
|
|
3263
3345
|
tab: activePanel,
|