@liiift-studio/sanity-visitor-insights 0.1.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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/index.d.mts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +652 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +609 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ranges-D6AZwpmm.d.mts +282 -0
- package/dist/ranges-D6AZwpmm.d.ts +282 -0
- package/dist/server.d.mts +280 -0
- package/dist/server.d.ts +280 -0
- package/dist/server.js +937 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +882 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +68 -0
- package/src/boundary.test.ts +93 -0
- package/src/core/core.test.ts +175 -0
- package/src/core/cutover.ts +109 -0
- package/src/core/ranges.ts +103 -0
- package/src/core/siteConfig.ts +150 -0
- package/src/index.ts +86 -0
- package/src/reportData.ts +120 -0
- package/src/server/auth.ts +133 -0
- package/src/server/cache.ts +75 -0
- package/src/server/createHandler.ts +199 -0
- package/src/server/ga4.ts +149 -0
- package/src/server/googleAuth.ts +139 -0
- package/src/server/orders.ts +127 -0
- package/src/server/reports/acquisition.ts +85 -0
- package/src/server/reports/journey.ts +112 -0
- package/src/server/reports/measurementHealth.ts +170 -0
- package/src/server/reports/typefaceInterest.ts +140 -0
- package/src/server/vercel.ts +68 -0
- package/src/server.ts +27 -0
- package/src/studio/Figure.tsx +175 -0
- package/src/studio/VisitorInsightsTool.tsx +247 -0
- package/src/studio/panels.tsx +233 -0
- package/src/studio/useReport.ts +99 -0
- package/src/types.ts +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liiift Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @liiift-studio/sanity-visitor-insights
|
|
2
|
+
|
|
3
|
+
Visitor-**behaviour** analytics inside Sanity Studio, for Darden, TDF and MCKL.
|
|
4
|
+
|
|
5
|
+
This is not a sales tool and does not duplicate the sales portal. It reports what visitors do —
|
|
6
|
+
where they come from, how far they get, which typefaces they engage with — and reconciles GA4
|
|
7
|
+
against Vercel so you can see how much of that behaviour you are actually measuring. Orders appear
|
|
8
|
+
only as a conversion anchor and a count; no revenue, and no customer data.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What it reports
|
|
13
|
+
|
|
14
|
+
| Panel | Question it answers |
|
|
15
|
+
|---|---|
|
|
16
|
+
| **Measurement health** | How much of reality does each source actually see? |
|
|
17
|
+
| **Acquisition** | Where do visitors come from, and how many are design-industry referrals? |
|
|
18
|
+
| **Journey** | How far do visitors get, and where do they stop? |
|
|
19
|
+
| **Typeface interest** | Which families get viewed, tested and bought? |
|
|
20
|
+
|
|
21
|
+
Each panel offers a week, quarter or year range.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Design decisions worth knowing
|
|
26
|
+
|
|
27
|
+
These are deliberate and load-bearing. Changing them changes whether the numbers are honest.
|
|
28
|
+
|
|
29
|
+
**Pageviews are compared to pageviews.** An earlier design subtracted Vercel pageviews from GA4
|
|
30
|
+
sessions and attributed the difference to consent, ad-blockers and bots. Sessions and pageviews are
|
|
31
|
+
different units, so that difference is dominated by the unit mismatch rather than by anything
|
|
32
|
+
missing — and GA4 exposes no signal separating those three causes anyway. The panel now compares
|
|
33
|
+
like with like and labels the remainder unexplained.
|
|
34
|
+
|
|
35
|
+
**Consent is measured, not inferred.** A `consent_granted` event turns the single largest cause of
|
|
36
|
+
the GA4/Vercel gap into a real number. Until a site instruments it, the panel says so instead of
|
|
37
|
+
guessing.
|
|
38
|
+
|
|
39
|
+
**A missing metric is never zero.** Every figure is a `MetricValue` tagged union, so an
|
|
40
|
+
uninstrumented event cannot silently coerce to `0` and be charted as a real trough. Unavailable
|
|
41
|
+
figures render as an em dash with a stated reason.
|
|
42
|
+
|
|
43
|
+
**The journey is a funnel, not a path.** GA4's Data API has no path-exploration endpoint, and
|
|
44
|
+
`runFunnelReport` returns step-conversion marginals rather than observed sequences. Drawing a flow
|
|
45
|
+
diagram from that would assert co-occurrence nobody measured. Steps are reported as independent
|
|
46
|
+
totals and the response is flagged approximate.
|
|
47
|
+
|
|
48
|
+
**Auth is the Studio's own session token.** The Studio forwards its Sanity token and the route
|
|
49
|
+
verifies it against Sanity. A shared secret would be extractable from the public Studio bundle,
|
|
50
|
+
moving the bar from "know the URL" to "open devtools", and would give no identity to audit.
|
|
51
|
+
|
|
52
|
+
**No charting library.** These panels compare and rank a handful of values, which labelled bars and
|
|
53
|
+
tables do as well as a chart — without a large Studio-bundle dependency or theme-token bridging for
|
|
54
|
+
light and dark. Tables are the accessible representation *and* the visual one, so they cannot drift
|
|
55
|
+
apart.
|
|
56
|
+
|
|
57
|
+
**No customer data leaves Sanity.** Order queries project `{_createdAt, orderStatus}` and, for the
|
|
58
|
+
interest panel, dereference only a typeface title. Joining behavioural data to order PII would turn
|
|
59
|
+
aggregate statistics into personal-data processing.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installing
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @liiift-studio/sanity-visitor-insights
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 1. Studio
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// sanity.config.ts
|
|
73
|
+
import { visitorInsights } from '@liiift-studio/sanity-visitor-insights'
|
|
74
|
+
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
plugins: [
|
|
77
|
+
visitorInsights({
|
|
78
|
+
apiBaseUrl: 'https://dardenstudio.com',
|
|
79
|
+
siteLabel: 'Darden Studio',
|
|
80
|
+
roles: ['administrator'],
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Pass `apiBaseUrl: ''` when the Studio is served from the same origin as the site (Darden and TDF).
|
|
87
|
+
MCKL's Studio deploys separately to `mckl.sanity.studio`, so it needs the full site URL.
|
|
88
|
+
|
|
89
|
+
Omit `roles` to show the tool to every Studio user. These panels read order-derived conversion
|
|
90
|
+
figures, so gating to administrators is usually right.
|
|
91
|
+
|
|
92
|
+
### 2. Site API route
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// pages/api/visitor-insights/[report].js
|
|
96
|
+
import { createVisitorInsightsHandler } from '@liiift-studio/sanity-visitor-insights/server'
|
|
97
|
+
import { client } from '../../../lib/sanityClient'
|
|
98
|
+
|
|
99
|
+
export default createVisitorInsightsHandler({
|
|
100
|
+
sanityClient: client,
|
|
101
|
+
config: {
|
|
102
|
+
siteId: 'darden',
|
|
103
|
+
label: 'Darden Studio',
|
|
104
|
+
ga4: { propertyId: '123456789', timezone: 'America/New_York' },
|
|
105
|
+
vercel: { projectId: 'prj_...' },
|
|
106
|
+
orders: { documentType: 'order', typefacesField: 'typefaces' },
|
|
107
|
+
eventCutovers: {
|
|
108
|
+
page_view: 'preexisting',
|
|
109
|
+
view_item: 'preexisting',
|
|
110
|
+
add_to_cart: 'preexisting',
|
|
111
|
+
begin_checkout: 'preexisting',
|
|
112
|
+
purchase: 'preexisting',
|
|
113
|
+
consent_granted: null,
|
|
114
|
+
tester_engaged: null,
|
|
115
|
+
},
|
|
116
|
+
// Only needed where the Studio is on a different origin.
|
|
117
|
+
allowedStudioOrigins: ['https://mckl.sanity.studio'],
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`propertyId` is the **numeric** GA4 property id from GA4 Admin, not the `G-XXXXXXX` measurement id.
|
|
123
|
+
The config is validated at construction and will throw on deploy rather than silently returning
|
|
124
|
+
empty charts — pasting a measurement id here is caught by name.
|
|
125
|
+
|
|
126
|
+
### 3. Environment variables
|
|
127
|
+
|
|
128
|
+
| Variable | Where | What |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `VISITOR_INSIGHTS_GA4_SERVICE_ACCOUNT` | Site (server) | Service-account JSON, raw or base64 |
|
|
131
|
+
| `VISITOR_INSIGHTS_VERCEL_TOKEN` | Site (server) | Vercel API token with project read access |
|
|
132
|
+
| `SANITY_STUDIO_PROJECT_ID` | Site (server) | Already set; used to verify Studio tokens |
|
|
133
|
+
|
|
134
|
+
The service account needs **Viewer** on each GA4 property. Nothing here is `NEXT_PUBLIC_`; none of
|
|
135
|
+
it reaches the browser.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Before it will show anything useful
|
|
140
|
+
|
|
141
|
+
- **Set GA4 data retention to 14 months** on every property (Admin → Data Settings → Data
|
|
142
|
+
Retention). The default may be 2 months, which makes the year range return nothing for most of
|
|
143
|
+
its span. It does not backfill, so every day at the default is data lost permanently.
|
|
144
|
+
- **Register custom dimensions before deploying the events that populate them.** A dimension only
|
|
145
|
+
reports from its registration date forward.
|
|
146
|
+
- **Instrument `consent_granted` and `tester_engaged`.** Without them the measurement-health
|
|
147
|
+
residual stays unexplained and the journey funnel has a hole where the tester belongs.
|
|
148
|
+
- **`tester_engaged` must require a change from the tester's default state** — custom text, or a
|
|
149
|
+
weight/size change. Firing on tester open counts every page load as a test and makes the
|
|
150
|
+
viewed-to-tested ratio meaningless.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm install
|
|
158
|
+
npm test # pure logic plus the client/server boundary guard
|
|
159
|
+
npm run build
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The boundary test walks the real import graph from each entry point and fails if anything reachable
|
|
163
|
+
from the Studio entry touches `src/server/`, `node:crypto` or `process.env`. That check exists
|
|
164
|
+
because the failure it prevents — a credential-reading module shipped in a public Studio bundle —
|
|
165
|
+
is invisible to type checking and to review.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Architecture
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
src/
|
|
173
|
+
types.ts MetricValue, ranges, report envelope
|
|
174
|
+
reportData.ts Result shapes shared by server and panels, importing neither
|
|
175
|
+
core/ Pure logic: cutovers, range resolution, config validation
|
|
176
|
+
server/ Node only — GA4, Vercel, Sanity, auth, cache, handler factory
|
|
177
|
+
studio/ Browser only — the tool, panels and figure renderers
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`core/` and `reportData.ts` are shared. Nothing in `studio/` may import from `server/`, which is
|
|
181
|
+
what the split export subpaths and the boundary test enforce together.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as sanity from 'sanity';
|
|
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';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Visitor Insights Studio tool.
|
|
8
|
+
*
|
|
9
|
+
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
10
|
+
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
11
|
+
* inconsistent between them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Props supplied by the plugin config. */
|
|
15
|
+
interface VisitorInsightsToolProps {
|
|
16
|
+
apiBaseUrl: string;
|
|
17
|
+
siteLabel: string;
|
|
18
|
+
}
|
|
19
|
+
/** The tool itself. */
|
|
20
|
+
declare function VisitorInsightsTool({ apiBaseUrl, siteLabel }: VisitorInsightsToolProps): React.ReactElement;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Data hook for the Studio panels.
|
|
24
|
+
*
|
|
25
|
+
* Forwards the Studio's own Sanity session token to the site's API route, which verifies it against
|
|
26
|
+
* Sanity. There is no shared secret: the Studio bundle is public, so anything compiled into it is
|
|
27
|
+
* extractable.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Loading state for a report. */
|
|
31
|
+
type ReportState<T> = {
|
|
32
|
+
status: 'idle';
|
|
33
|
+
} | {
|
|
34
|
+
status: 'loading';
|
|
35
|
+
} | {
|
|
36
|
+
status: 'ready';
|
|
37
|
+
envelope: ReportEnvelope<T>;
|
|
38
|
+
} | {
|
|
39
|
+
status: 'error';
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
/** Options for useReport. */
|
|
43
|
+
interface UseReportOptions {
|
|
44
|
+
/** Base URL of the site serving the reports, e.g. `https://dardenstudio.com`. */
|
|
45
|
+
apiBaseUrl: string;
|
|
46
|
+
report: ReportName;
|
|
47
|
+
range: RangeKey;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetch a report, re-fetching when the range changes.
|
|
51
|
+
*
|
|
52
|
+
* @returns the current state plus a `reload` for manual refresh
|
|
53
|
+
*/
|
|
54
|
+
declare function useReport<T>({ apiBaseUrl, report, range }: UseReportOptions): {
|
|
55
|
+
state: ReportState<T>;
|
|
56
|
+
reload: () => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shared renderers for metric values and comparison bars.
|
|
61
|
+
*
|
|
62
|
+
* Two rules are enforced here rather than left to each panel:
|
|
63
|
+
*
|
|
64
|
+
* 1. An unavailable metric renders as an em dash with a stated reason — never as "0". A missing
|
|
65
|
+
* measurement and a measured zero mean opposite things, and must never look alike.
|
|
66
|
+
* 2. Nothing is distinguished by colour alone. Bars carry a text value, a percentage and an
|
|
67
|
+
* accessible label, so the figure survives greyscale, colour blindness and both Studio themes.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/** Format a number with thousands separators. */
|
|
71
|
+
declare function formatCount(value: number): string;
|
|
72
|
+
/** Format a 0–1 ratio as a percentage. */
|
|
73
|
+
declare function formatPercent(ratio: number, digits?: number): string;
|
|
74
|
+
/** Props for MetricFigure. */
|
|
75
|
+
interface MetricFigureProps {
|
|
76
|
+
metric: MetricValue;
|
|
77
|
+
/** Accessible label describing what this number counts. */
|
|
78
|
+
label: string;
|
|
79
|
+
size?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a metric value, handling the absent case visibly.
|
|
83
|
+
* Screen readers get the reason text rather than an unexplained dash.
|
|
84
|
+
*/
|
|
85
|
+
declare function MetricFigure({ metric, label, size }: MetricFigureProps): React.ReactElement;
|
|
86
|
+
/** Props for ComparisonBar. */
|
|
87
|
+
interface ComparisonBarProps {
|
|
88
|
+
label: string;
|
|
89
|
+
metric: MetricValue;
|
|
90
|
+
/** Largest value across the sibling bars, used to scale width. */
|
|
91
|
+
max: number;
|
|
92
|
+
/** Tone conveys category, but never carries meaning on its own. */
|
|
93
|
+
tone?: 'primary' | 'positive' | 'caution' | 'default';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A horizontal bar with its value printed alongside.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately CSS rather than a charting library: these panels compare and rank a handful of
|
|
99
|
+
* values, which a labelled bar does as well as a chart while avoiding a large dependency in the
|
|
100
|
+
* Studio bundle and the theme-token bridging that a chart library would need for light and dark.
|
|
101
|
+
*/
|
|
102
|
+
declare function ComparisonBar({ label, metric, max, tone }: ComparisonBarProps): React.ReactElement;
|
|
103
|
+
/** Props for NoticeList. */
|
|
104
|
+
interface NoticeListProps {
|
|
105
|
+
notices: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Caveats attached to a report — sampling, processing lag, instrumentation cutovers.
|
|
109
|
+
* Rendered in the panel rather than a README: a caveat nobody sees does not prevent a wrong read.
|
|
110
|
+
*/
|
|
111
|
+
declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Studio entry point — browser only.
|
|
115
|
+
*
|
|
116
|
+
* Must never import from `./server` or anything under `src/server/`, which reads credentials and
|
|
117
|
+
* uses node:crypto. The split export subpaths are what keep that guarantee enforceable.
|
|
118
|
+
*/
|
|
119
|
+
/** Options for the Studio plugin. */
|
|
120
|
+
interface VisitorInsightsPluginOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Base URL of the site serving the reports, e.g. `https://dardenstudio.com`.
|
|
123
|
+
* Same-origin studios can pass an empty string.
|
|
124
|
+
*/
|
|
125
|
+
apiBaseUrl: string;
|
|
126
|
+
/** Label shown in the tool header. */
|
|
127
|
+
siteLabel: string;
|
|
128
|
+
/** Tool name in the Studio URL. Defaults to `visitor-insights`. */
|
|
129
|
+
name?: string;
|
|
130
|
+
/** Title in the Studio nav. Defaults to `Insights`. */
|
|
131
|
+
title?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Restrict the tool to these Sanity roles. Omit to show it to every Studio user.
|
|
134
|
+
* These panels read order-derived conversion figures, so gating to administrators is
|
|
135
|
+
* usually right — matching how the deploy and utilities tools are already gated.
|
|
136
|
+
*/
|
|
137
|
+
roles?: string[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Visitor Insights — visitor-behaviour analytics inside the Studio.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* plugins: [
|
|
145
|
+
* visitorInsights({
|
|
146
|
+
* apiBaseUrl: 'https://dardenstudio.com',
|
|
147
|
+
* siteLabel: 'Darden Studio',
|
|
148
|
+
* roles: ['administrator'],
|
|
149
|
+
* }),
|
|
150
|
+
* ]
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
154
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as sanity from 'sanity';
|
|
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';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Visitor Insights Studio tool.
|
|
8
|
+
*
|
|
9
|
+
* One range control drives every panel, implemented once here as a proper radiogroup with
|
|
10
|
+
* roving tabindex — repeating a bespoke toggle per panel is how keyboard behaviour ends up
|
|
11
|
+
* inconsistent between them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Props supplied by the plugin config. */
|
|
15
|
+
interface VisitorInsightsToolProps {
|
|
16
|
+
apiBaseUrl: string;
|
|
17
|
+
siteLabel: string;
|
|
18
|
+
}
|
|
19
|
+
/** The tool itself. */
|
|
20
|
+
declare function VisitorInsightsTool({ apiBaseUrl, siteLabel }: VisitorInsightsToolProps): React.ReactElement;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Data hook for the Studio panels.
|
|
24
|
+
*
|
|
25
|
+
* Forwards the Studio's own Sanity session token to the site's API route, which verifies it against
|
|
26
|
+
* Sanity. There is no shared secret: the Studio bundle is public, so anything compiled into it is
|
|
27
|
+
* extractable.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Loading state for a report. */
|
|
31
|
+
type ReportState<T> = {
|
|
32
|
+
status: 'idle';
|
|
33
|
+
} | {
|
|
34
|
+
status: 'loading';
|
|
35
|
+
} | {
|
|
36
|
+
status: 'ready';
|
|
37
|
+
envelope: ReportEnvelope<T>;
|
|
38
|
+
} | {
|
|
39
|
+
status: 'error';
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
/** Options for useReport. */
|
|
43
|
+
interface UseReportOptions {
|
|
44
|
+
/** Base URL of the site serving the reports, e.g. `https://dardenstudio.com`. */
|
|
45
|
+
apiBaseUrl: string;
|
|
46
|
+
report: ReportName;
|
|
47
|
+
range: RangeKey;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetch a report, re-fetching when the range changes.
|
|
51
|
+
*
|
|
52
|
+
* @returns the current state plus a `reload` for manual refresh
|
|
53
|
+
*/
|
|
54
|
+
declare function useReport<T>({ apiBaseUrl, report, range }: UseReportOptions): {
|
|
55
|
+
state: ReportState<T>;
|
|
56
|
+
reload: () => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shared renderers for metric values and comparison bars.
|
|
61
|
+
*
|
|
62
|
+
* Two rules are enforced here rather than left to each panel:
|
|
63
|
+
*
|
|
64
|
+
* 1. An unavailable metric renders as an em dash with a stated reason — never as "0". A missing
|
|
65
|
+
* measurement and a measured zero mean opposite things, and must never look alike.
|
|
66
|
+
* 2. Nothing is distinguished by colour alone. Bars carry a text value, a percentage and an
|
|
67
|
+
* accessible label, so the figure survives greyscale, colour blindness and both Studio themes.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/** Format a number with thousands separators. */
|
|
71
|
+
declare function formatCount(value: number): string;
|
|
72
|
+
/** Format a 0–1 ratio as a percentage. */
|
|
73
|
+
declare function formatPercent(ratio: number, digits?: number): string;
|
|
74
|
+
/** Props for MetricFigure. */
|
|
75
|
+
interface MetricFigureProps {
|
|
76
|
+
metric: MetricValue;
|
|
77
|
+
/** Accessible label describing what this number counts. */
|
|
78
|
+
label: string;
|
|
79
|
+
size?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a metric value, handling the absent case visibly.
|
|
83
|
+
* Screen readers get the reason text rather than an unexplained dash.
|
|
84
|
+
*/
|
|
85
|
+
declare function MetricFigure({ metric, label, size }: MetricFigureProps): React.ReactElement;
|
|
86
|
+
/** Props for ComparisonBar. */
|
|
87
|
+
interface ComparisonBarProps {
|
|
88
|
+
label: string;
|
|
89
|
+
metric: MetricValue;
|
|
90
|
+
/** Largest value across the sibling bars, used to scale width. */
|
|
91
|
+
max: number;
|
|
92
|
+
/** Tone conveys category, but never carries meaning on its own. */
|
|
93
|
+
tone?: 'primary' | 'positive' | 'caution' | 'default';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A horizontal bar with its value printed alongside.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately CSS rather than a charting library: these panels compare and rank a handful of
|
|
99
|
+
* values, which a labelled bar does as well as a chart while avoiding a large dependency in the
|
|
100
|
+
* Studio bundle and the theme-token bridging that a chart library would need for light and dark.
|
|
101
|
+
*/
|
|
102
|
+
declare function ComparisonBar({ label, metric, max, tone }: ComparisonBarProps): React.ReactElement;
|
|
103
|
+
/** Props for NoticeList. */
|
|
104
|
+
interface NoticeListProps {
|
|
105
|
+
notices: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Caveats attached to a report — sampling, processing lag, instrumentation cutovers.
|
|
109
|
+
* Rendered in the panel rather than a README: a caveat nobody sees does not prevent a wrong read.
|
|
110
|
+
*/
|
|
111
|
+
declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Studio entry point — browser only.
|
|
115
|
+
*
|
|
116
|
+
* Must never import from `./server` or anything under `src/server/`, which reads credentials and
|
|
117
|
+
* uses node:crypto. The split export subpaths are what keep that guarantee enforceable.
|
|
118
|
+
*/
|
|
119
|
+
/** Options for the Studio plugin. */
|
|
120
|
+
interface VisitorInsightsPluginOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Base URL of the site serving the reports, e.g. `https://dardenstudio.com`.
|
|
123
|
+
* Same-origin studios can pass an empty string.
|
|
124
|
+
*/
|
|
125
|
+
apiBaseUrl: string;
|
|
126
|
+
/** Label shown in the tool header. */
|
|
127
|
+
siteLabel: string;
|
|
128
|
+
/** Tool name in the Studio URL. Defaults to `visitor-insights`. */
|
|
129
|
+
name?: string;
|
|
130
|
+
/** Title in the Studio nav. Defaults to `Insights`. */
|
|
131
|
+
title?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Restrict the tool to these Sanity roles. Omit to show it to every Studio user.
|
|
134
|
+
* These panels read order-derived conversion figures, so gating to administrators is
|
|
135
|
+
* usually right — matching how the deploy and utilities tools are already gated.
|
|
136
|
+
*/
|
|
137
|
+
roles?: string[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Visitor Insights — visitor-behaviour analytics inside the Studio.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* plugins: [
|
|
145
|
+
* visitorInsights({
|
|
146
|
+
* apiBaseUrl: 'https://dardenstudio.com',
|
|
147
|
+
* siteLabel: 'Darden Studio',
|
|
148
|
+
* roles: ['administrator'],
|
|
149
|
+
* }),
|
|
150
|
+
* ]
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
154
|
+
|
|
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 };
|