@liiift-studio/sanity-visitor-insights 0.1.0 → 0.2.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 CHANGED
@@ -17,6 +17,7 @@ only as a conversion anchor and a count; no revenue, and no customer data.
17
17
  | **Acquisition** | Where do visitors come from, and how many are design-industry referrals? |
18
18
  | **Journey** | How far do visitors get, and where do they stop? |
19
19
  | **Typeface interest** | Which families get viewed, tested and bought? |
20
+ | **Diagnostics** | What to fix before trusting any of the above? |
20
21
 
21
22
  Each panel offers a week, quarter or year range.
22
23
 
@@ -60,6 +61,53 @@ aggregate statistics into personal-data processing.
60
61
 
61
62
  ---
62
63
 
64
+ ## Diagnostics — run this first
65
+
66
+ The panels are only as trustworthy as the property behind them, and the ways a GA4 property
67
+ quietly produces wrong numbers are not visible from the numbers themselves. The Diagnostics panel
68
+ measures them:
69
+
70
+ - **GA4 reachable** — distinguishes a bad service-account key from missing Viewer access.
71
+ - **Timezone matches config** — a mismatch shifts day and week boundaries, so GA4, Vercel and
72
+ Sanity stop agreeing which period an event belongs to.
73
+ - **Retention covers a year** — probes a window beyond the 2-month default rather than asking you
74
+ to read the Admin screen. This is the check most likely to explain an empty year range.
75
+ - **Configured events fire** — catches a cutover map claiming an event is live when GA4 has never
76
+ seen it, which otherwise produces figures that look real but are not.
77
+ - **Purchases carry `transaction_id`** — without it a GA4 purchase cannot be reconciled against a
78
+ Sanity order at all.
79
+ - **GA4 purchases match orders** — a large divergence usually means `purchase` fires on a page some
80
+ buyers never reach, fires twice, or is being blocked.
81
+
82
+ It is also the one panel that still says something useful with nothing configured, so it is worth
83
+ opening the moment credentials land. Available in the Studio, or headless:
84
+
85
+ ```ts
86
+ import { runDiagnostics } from '@liiift-studio/sanity-visitor-insights/server'
87
+
88
+ const report = await runDiagnostics({ config, ga4, vercel, sanity })
89
+ console.log(report.verdict, report.checks)
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Testing without credentials
95
+
96
+ Test doubles ship from a separate subpath, so a consuming site can exercise these reports before
97
+ it has anything to point at:
98
+
99
+ ```ts
100
+ import { createFakeGa4Client, makeGa4Total } from '@liiift-studio/sanity-visitor-insights/testing'
101
+
102
+ const ga4 = createFakeGa4Client({ batch: () => [makeGa4Total(800), makeGa4Total(300), makeGa4Total(0)] })
103
+ const data = await measurementHealth({ config, range, ga4, vercel: null, sanity: null })
104
+ ```
105
+
106
+ The fakes record what they were asked, so a test can assert on the GROQ that ran — which is how
107
+ this package proves no customer field is ever projected.
108
+
109
+ ---
110
+
63
111
  ## Installing
64
112
 
65
113
  ```bash
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 } from './ranges-D6AZwpmm.mjs';
4
- export { C as Coverage, D as DateRange, E as EventCutover, P as PREEXISTING, c as REPORT_NAMES, d as ReportError, S as SiteAnalyticsConfig, e as SourceName, f as SourceStatus, U as UnavailableReason, g as coverageForRange, i as isReportName, o as ok, p as partial, h as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, j as valueOrNull } from './ranges-D6AZwpmm.mjs';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DSFapooC.mjs';
4
+ export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-DSFapooC.mjs';
5
5
 
6
6
  /**
7
7
  * The Visitor Insights Studio tool.
@@ -110,6 +110,45 @@ interface NoticeListProps {
110
110
  */
111
111
  declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
112
112
 
113
+ /**
114
+ * The four report panels.
115
+ *
116
+ * Every panel renders a table or labelled bars rather than a chart, and every one surfaces its own
117
+ * caveats inline. Tables are the accessible representation as well as the visual one, so there is
118
+ * no separate "view as data" toggle that could drift out of sync with what is displayed.
119
+ */
120
+
121
+ /**
122
+ * Measurement Health — how much of reality each source sees.
123
+ * Compares pageviews to pageviews. Sessions and orders sit alongside as context and are never
124
+ * subtracted from a pageview count.
125
+ */
126
+ declare function MeasurementHealthPanel({ data }: {
127
+ data: MeasurementHealthData;
128
+ }): React.ReactElement;
129
+ /** Acquisition — where visitors came from, with design-industry referrers called out. */
130
+ declare function AcquisitionPanel({ data }: {
131
+ data: AcquisitionData;
132
+ }): React.ReactElement;
133
+ /** Journey — per-step totals with adjacent drop-off, explicitly not a tracked path. */
134
+ declare function JourneyPanel({ data }: {
135
+ data: JourneyData;
136
+ }): React.ReactElement;
137
+ /** Typeface interest — viewed, tested and bought per family. */
138
+ declare function TypefaceInterestPanel({ data }: {
139
+ data: TypefaceInterestData;
140
+ }): React.ReactElement;
141
+ /**
142
+ * Diagnostics — what to fix before trusting anything else here.
143
+ *
144
+ * Deliberately the panel that still works with nothing configured: on a site without credentials
145
+ * it is the only one that can say something useful, and it is the first thing worth opening once
146
+ * credentials land.
147
+ */
148
+ declare function DiagnosticsPanel({ data }: {
149
+ data: DiagnosticReport;
150
+ }): React.ReactElement;
151
+
113
152
  /**
114
153
  * Studio entry point — browser only.
115
154
  *
@@ -152,4 +191,4 @@ interface VisitorInsightsPluginOptions {
152
191
  */
153
192
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
154
193
 
155
- export { ComparisonBar, MetricFigure, MetricValue, NoticeList, RangeKey, ReportEnvelope, ReportName, type ReportState, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatPercent, useReport, visitorInsights };
194
+ export { AcquisitionPanel, ComparisonBar, DiagnosticsPanel, JourneyPanel, MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, RangeKey, ReportEnvelope, ReportName, type ReportState, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatPercent, useReport, visitorInsights };
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 } from './ranges-D6AZwpmm.js';
4
- export { C as Coverage, D as DateRange, E as EventCutover, P as PREEXISTING, c as REPORT_NAMES, d as ReportError, S as SiteAnalyticsConfig, e as SourceName, f as SourceStatus, U as UnavailableReason, g as coverageForRange, i as isReportName, o as ok, p as partial, h as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, j as valueOrNull } from './ranges-D6AZwpmm.js';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DSFapooC.js';
4
+ export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-DSFapooC.js';
5
5
 
6
6
  /**
7
7
  * The Visitor Insights Studio tool.
@@ -110,6 +110,45 @@ interface NoticeListProps {
110
110
  */
111
111
  declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
112
112
 
113
+ /**
114
+ * The four report panels.
115
+ *
116
+ * Every panel renders a table or labelled bars rather than a chart, and every one surfaces its own
117
+ * caveats inline. Tables are the accessible representation as well as the visual one, so there is
118
+ * no separate "view as data" toggle that could drift out of sync with what is displayed.
119
+ */
120
+
121
+ /**
122
+ * Measurement Health — how much of reality each source sees.
123
+ * Compares pageviews to pageviews. Sessions and orders sit alongside as context and are never
124
+ * subtracted from a pageview count.
125
+ */
126
+ declare function MeasurementHealthPanel({ data }: {
127
+ data: MeasurementHealthData;
128
+ }): React.ReactElement;
129
+ /** Acquisition — where visitors came from, with design-industry referrers called out. */
130
+ declare function AcquisitionPanel({ data }: {
131
+ data: AcquisitionData;
132
+ }): React.ReactElement;
133
+ /** Journey — per-step totals with adjacent drop-off, explicitly not a tracked path. */
134
+ declare function JourneyPanel({ data }: {
135
+ data: JourneyData;
136
+ }): React.ReactElement;
137
+ /** Typeface interest — viewed, tested and bought per family. */
138
+ declare function TypefaceInterestPanel({ data }: {
139
+ data: TypefaceInterestData;
140
+ }): React.ReactElement;
141
+ /**
142
+ * Diagnostics — what to fix before trusting anything else here.
143
+ *
144
+ * Deliberately the panel that still works with nothing configured: on a site without credentials
145
+ * it is the only one that can say something useful, and it is the first thing worth opening once
146
+ * credentials land.
147
+ */
148
+ declare function DiagnosticsPanel({ data }: {
149
+ data: DiagnosticReport;
150
+ }): React.ReactElement;
151
+
113
152
  /**
114
153
  * Studio entry point — browser only.
115
154
  *
@@ -152,4 +191,4 @@ interface VisitorInsightsPluginOptions {
152
191
  */
153
192
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
154
193
 
155
- export { ComparisonBar, MetricFigure, MetricValue, NoticeList, RangeKey, ReportEnvelope, ReportName, type ReportState, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatPercent, useReport, visitorInsights };
194
+ export { AcquisitionPanel, ComparisonBar, DiagnosticsPanel, JourneyPanel, MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, RangeKey, ReportEnvelope, ReportName, type ReportState, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatPercent, useReport, visitorInsights };
package/dist/index.js CHANGED
@@ -20,11 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AcquisitionPanel: () => AcquisitionPanel,
23
24
  ComparisonBar: () => ComparisonBar,
25
+ DiagnosticsPanel: () => DiagnosticsPanel,
26
+ JourneyPanel: () => JourneyPanel,
27
+ MeasurementHealthPanel: () => MeasurementHealthPanel,
24
28
  MetricFigure: () => MetricFigure,
25
29
  NoticeList: () => NoticeList,
26
30
  PREEXISTING: () => PREEXISTING,
27
31
  REPORT_NAMES: () => REPORT_NAMES,
32
+ TypefaceInterestPanel: () => TypefaceInterestPanel,
28
33
  VisitorInsightsTool: () => VisitorInsightsTool,
29
34
  coverageForRange: () => coverageForRange,
30
35
  default: () => index_default,
@@ -313,6 +318,34 @@ function TypefaceInterestPanel({ data }) {
313
318
  ] }) })
314
319
  ] });
315
320
  }
321
+ var CHECK_TONE = {
322
+ pass: "positive",
323
+ warn: "caution",
324
+ fail: "critical",
325
+ skipped: "default"
326
+ };
327
+ var CHECK_WORD = {
328
+ pass: "Pass",
329
+ warn: "Check",
330
+ fail: "Fail",
331
+ skipped: "Skipped"
332
+ };
333
+ function DiagnosticsPanel({ data }) {
334
+ const failing = data.checks.filter((c) => c.status === "fail").length;
335
+ const warning = data.checks.filter((c) => c.status === "warn").length;
336
+ const summary = data.verdict === "pass" ? "Everything checked out. The figures in the other panels can be taken at face value." : `${failing} failing, ${warning} worth a look. Panels depending on these will be wrong or incomplete until they are resolved.`;
337
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
338
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: CHECK_TONE[data.verdict], border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: summary }) }),
339
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: data.checks.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
340
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { align: "center", justify: "space-between", gap: 3, children: [
341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: item.label }),
342
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 2, radius: 2, tone: CHECK_TONE[item.status], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, weight: "medium", children: CHECK_WORD[item.status] }) })
343
+ ] }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: item.detail }),
345
+ item.remedy && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: item.remedy })
346
+ ] }) }, item.id)) })
347
+ ] });
348
+ }
316
349
 
317
350
  // src/studio/VisitorInsightsTool.tsx
318
351
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -325,7 +358,8 @@ var PANELS = [
325
358
  { report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
326
359
  { report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
327
360
  { report: "journey", label: "Journey", blurb: "How far visitors get, and where they stop" },
328
- { report: "typeface-interest", label: "Typeface interest", blurb: "Viewed, tested and bought, by family" }
361
+ { report: "typeface-interest", label: "Typeface interest", blurb: "Viewed, tested and bought, by family" },
362
+ { report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
329
363
  ];
330
364
  function RangeSelector({ value, onChange }) {
331
365
  const refs = (0, import_react2.useRef)([]);
@@ -430,6 +464,7 @@ function ReportPanel({ report, apiBaseUrl, range }) {
430
464
  report === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AcquisitionPanel, { data: state.envelope.data }),
431
465
  report === "journey" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(JourneyPanel, { data: state.envelope.data }),
432
466
  report === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
467
+ report === "diagnostics" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: state.envelope.data }),
433
468
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
434
469
  state.envelope.range.start,
435
470
  " to ",
@@ -488,7 +523,8 @@ var REPORT_NAMES = [
488
523
  "measurement-health",
489
524
  "acquisition",
490
525
  "journey",
491
- "typeface-interest"
526
+ "typeface-interest",
527
+ "diagnostics"
492
528
  ];
493
529
  function isReportName(value) {
494
530
  return typeof value === "string" && REPORT_NAMES.includes(value);
@@ -629,11 +665,16 @@ var visitorInsights = (0, import_sanity2.definePlugin)((options) => {
629
665
  var index_default = visitorInsights;
630
666
  // Annotate the CommonJS export names for ESM import in node:
631
667
  0 && (module.exports = {
668
+ AcquisitionPanel,
632
669
  ComparisonBar,
670
+ DiagnosticsPanel,
671
+ JourneyPanel,
672
+ MeasurementHealthPanel,
633
673
  MetricFigure,
634
674
  NoticeList,
635
675
  PREEXISTING,
636
676
  REPORT_NAMES,
677
+ TypefaceInterestPanel,
637
678
  VisitorInsightsTool,
638
679
  coverageForRange,
639
680
  formatCount,