@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/dist/server.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { S as SiteAnalyticsConfig, M as MetricValue } from './ranges-D6AZwpmm.js';
|
|
2
|
+
export { k as ConfigProblem, C as Coverage, D as DateRange, E as EventCutover, G as GA4_PROCESSING_LAG_DAYS, l as Ga4Config, O as OrdersConfig, P as PREEXISTING, c as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, d as ReportError, a as ReportName, e as SourceName, f as SourceStatus, U as UnavailableReason, V as VercelConfig, m as applyCoverage, n as assertValidSiteConfig, g as coverageForRange, q as coverageNotices, s as daysBetween, t as formatInTimeZone, i as isReportName, w as isValidTimeZone, o as ok, p as partial, h as previousRange, x as provisionalDates, y as provisionalNotice, r as resolveRange, z as shiftDays, u as unavailable, v as validateSiteConfig, j as valueOrNull } from './ranges-D6AZwpmm.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Request authentication and CORS for the report handler.
|
|
6
|
+
*
|
|
7
|
+
* The Studio forwards its own Sanity session token and this verifies it against Sanity. A shared
|
|
8
|
+
* secret was the obvious alternative and is the wrong one: the Studio bundle is served publicly, so
|
|
9
|
+
* any secret compiled into it is extractable, which moves the bar from "know the URL" to "open
|
|
10
|
+
* devtools". Verifying a session token instead proves the caller is a logged-in project user, and
|
|
11
|
+
* gives a real identity to log rather than an anonymous caller who read a constant.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors the pattern already in production on Darden's order-action endpoints.
|
|
14
|
+
*/
|
|
15
|
+
/** Minimal shape this module needs from a Next.js API request. */
|
|
16
|
+
interface HandlerRequest {
|
|
17
|
+
method?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
headers: Record<string, string | string[] | undefined>;
|
|
20
|
+
query?: Record<string, string | string[] | undefined>;
|
|
21
|
+
}
|
|
22
|
+
/** Minimal shape this module needs from a Next.js API response. */
|
|
23
|
+
interface HandlerResponse {
|
|
24
|
+
setHeader(name: string, value: string): void;
|
|
25
|
+
status(code: number): HandlerResponse;
|
|
26
|
+
json(body: unknown): void;
|
|
27
|
+
end(): void;
|
|
28
|
+
}
|
|
29
|
+
/** A verified Sanity Studio user. */
|
|
30
|
+
interface StudioUser {
|
|
31
|
+
id: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
roles?: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Outcome of verifying a request's Studio token. */
|
|
39
|
+
type VerifyResult = {
|
|
40
|
+
ok: true;
|
|
41
|
+
user: StudioUser;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Verify that a request carries a valid Sanity user token for this project.
|
|
48
|
+
*
|
|
49
|
+
* @param req - the incoming request
|
|
50
|
+
* @param sanityProjectId - project the token must belong to
|
|
51
|
+
* @returns ok with the resolved user, or a reason suitable for logging but never for the response
|
|
52
|
+
*/
|
|
53
|
+
declare function verifyStudioRequest(req: HandlerRequest, sanityProjectId: string): Promise<VerifyResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Apply CORS headers for a Studio-originated request and answer the preflight.
|
|
56
|
+
*
|
|
57
|
+
* Echoes one allow-listed origin rather than sending `*`. A wildcard is acceptable for public HTML
|
|
58
|
+
* but not for an endpoint that takes an Authorization header and returns business data.
|
|
59
|
+
*
|
|
60
|
+
* @param req - the incoming request
|
|
61
|
+
* @param res - the response to decorate
|
|
62
|
+
* @param allowedOrigins - exact origins permitted to call cross-origin
|
|
63
|
+
* @returns true when the request was a preflight and is now fully answered
|
|
64
|
+
*/
|
|
65
|
+
declare function applyCors(req: HandlerRequest, res: HandlerResponse, allowedOrigins: readonly string[]): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Guard a report endpoint. Responds 401 and returns null when the caller cannot be verified.
|
|
68
|
+
*
|
|
69
|
+
* @returns the verified user, or null when the request has already been answered with a 401
|
|
70
|
+
*/
|
|
71
|
+
declare function requireStudioUser(req: HandlerRequest, res: HandlerResponse, sanityProjectId: string): Promise<StudioUser | null>;
|
|
72
|
+
|
|
73
|
+
/** What this module needs from a Sanity client — kept minimal so it is trivial to stub in tests. */
|
|
74
|
+
interface SanityQueryClient {
|
|
75
|
+
fetch<T>(query: string, params?: Record<string, unknown>): Promise<T>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The mountable Next.js API-route handler.
|
|
80
|
+
*
|
|
81
|
+
* Ships from this package rather than being copy-pasted into each site so the three foundries
|
|
82
|
+
* cannot drift apart: a fix applied here reaches all of them on a version bump, instead of being
|
|
83
|
+
* applied to one repo and forgotten in the other two. A consuming site's route is then:
|
|
84
|
+
*
|
|
85
|
+
* // pages/api/visitor-insights/[report].js
|
|
86
|
+
* import { createVisitorInsightsHandler } from '@liiift-studio/sanity-visitor-insights/server'
|
|
87
|
+
* export default createVisitorInsightsHandler({ config: mySiteConfig })
|
|
88
|
+
*
|
|
89
|
+
* Credentials are read from the environment inside this module and never leave the server.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/** Environment variables this handler reads. Names are fixed so all three sites match. */
|
|
93
|
+
declare const ENV_VARS: {
|
|
94
|
+
/** Service-account JSON (raw or base64) with Viewer on the GA4 property. */
|
|
95
|
+
readonly googleServiceAccount: "VISITOR_INSIGHTS_GA4_SERVICE_ACCOUNT";
|
|
96
|
+
/** Vercel API token with read access to the project. */
|
|
97
|
+
readonly vercelToken: "VISITOR_INSIGHTS_VERCEL_TOKEN";
|
|
98
|
+
};
|
|
99
|
+
/** Options for building a handler. */
|
|
100
|
+
interface HandlerOptions {
|
|
101
|
+
/** This site's description of itself. */
|
|
102
|
+
config: SiteAnalyticsConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Sanity client used for order counts. Supply the site's existing server-side client so this
|
|
105
|
+
* package does not need its own Sanity credentials.
|
|
106
|
+
*/
|
|
107
|
+
sanityClient?: SanityQueryClient;
|
|
108
|
+
/** Sanity project id used to verify Studio tokens. Defaults to `SANITY_STUDIO_PROJECT_ID`. */
|
|
109
|
+
sanityProjectId?: string;
|
|
110
|
+
/** Cache lifetime for report responses. */
|
|
111
|
+
cacheTtlMs?: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the API-route handler for a site.
|
|
115
|
+
*
|
|
116
|
+
* @param options - the site config and its Sanity client
|
|
117
|
+
* @returns a Next.js Pages Router API handler
|
|
118
|
+
*/
|
|
119
|
+
declare function createVisitorInsightsHandler(options: HandlerOptions): (req: HandlerRequest, res: HandlerResponse) => Promise<void>;
|
|
120
|
+
|
|
121
|
+
/** Empty the cache. Test seam, and useful after a config change. */
|
|
122
|
+
declare function clearCache(): void;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Report result shapes, shared by the server that produces them and the panels that render them.
|
|
126
|
+
*
|
|
127
|
+
* These live outside both entry points on purpose. If the panels imported them from
|
|
128
|
+
* `src/server/reports/*` — even as `import type`, which does erase at build time — the module
|
|
129
|
+
* graph would still contain a client-to-server edge, and the guarantee that credential-reading
|
|
130
|
+
* code cannot reach a Studio bundle would rest on a compiler detail rather than on structure.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/** How much of reality each source sees. Pageviews compare like with like; the rest is context. */
|
|
134
|
+
interface MeasurementHealthData {
|
|
135
|
+
/** GA4 pageviews — directly comparable to `vercelPageviews`. */
|
|
136
|
+
ga4Pageviews: MetricValue;
|
|
137
|
+
/** Vercel pageviews — cookieless, not consent-gated. */
|
|
138
|
+
vercelPageviews: MetricValue;
|
|
139
|
+
/**
|
|
140
|
+
* Vercel minus GA4 pageviews, as a share of Vercel. Positive means GA4 saw less.
|
|
141
|
+
* Null when either side is unavailable — never computed against a missing operand.
|
|
142
|
+
*/
|
|
143
|
+
shortfallRatio: number | null;
|
|
144
|
+
/** GA4 sessions. Context only: a session is not a pageview and is never subtracted from one. */
|
|
145
|
+
ga4Sessions: MetricValue;
|
|
146
|
+
/** Orders in range. Ground truth for conversions, shown as context. */
|
|
147
|
+
orders: MetricValue;
|
|
148
|
+
/** Share of sessions that granted consent, where the event exists. */
|
|
149
|
+
consentRate: MetricValue;
|
|
150
|
+
/** Plain-language reading of the numbers above, for a non-analyst audience. */
|
|
151
|
+
interpretation: string;
|
|
152
|
+
}
|
|
153
|
+
/** One acquisition source. */
|
|
154
|
+
interface SourceRow {
|
|
155
|
+
/** GA4 `sessionSource` value. */
|
|
156
|
+
source: string;
|
|
157
|
+
/** GA4 `sessionDefaultChannelGroup`, e.g. Organic Search. */
|
|
158
|
+
channel: string;
|
|
159
|
+
sessions: number;
|
|
160
|
+
/** True when this source is one of the design-industry referrers. */
|
|
161
|
+
designIndustry: boolean;
|
|
162
|
+
/** True for GA4's `(not set)` / `(direct)` style buckets, which are not real sources. */
|
|
163
|
+
unattributed: boolean;
|
|
164
|
+
}
|
|
165
|
+
/** Where visitors came from. */
|
|
166
|
+
interface AcquisitionData {
|
|
167
|
+
rows: SourceRow[];
|
|
168
|
+
totalSessions: number;
|
|
169
|
+
/** Sessions from design-industry referrers, as a share of the total. */
|
|
170
|
+
designIndustryShare: number | null;
|
|
171
|
+
/** Sessions GA4 could not attribute, as a share of the total. */
|
|
172
|
+
unattributedShare: number | null;
|
|
173
|
+
/** True when GA4 withheld low-count rows, so the tail is shorter than reality. */
|
|
174
|
+
rowsWithheld: boolean;
|
|
175
|
+
}
|
|
176
|
+
/** One rung of the funnel. */
|
|
177
|
+
interface JourneyStep {
|
|
178
|
+
key: string;
|
|
179
|
+
label: string;
|
|
180
|
+
event: string;
|
|
181
|
+
count: MetricValue;
|
|
182
|
+
/**
|
|
183
|
+
* Share of the previous measurable step that reached this one.
|
|
184
|
+
* Null when either end is unavailable — a drop-off across an unmeasured step is meaningless.
|
|
185
|
+
*/
|
|
186
|
+
conversionFromPrevious: number | null;
|
|
187
|
+
}
|
|
188
|
+
/** A page where sessions commonly ended. */
|
|
189
|
+
interface ExitPage {
|
|
190
|
+
path: string;
|
|
191
|
+
exits: number;
|
|
192
|
+
}
|
|
193
|
+
/** How far visitors get. Per-step totals, never an observed path. */
|
|
194
|
+
interface JourneyData {
|
|
195
|
+
steps: JourneyStep[];
|
|
196
|
+
topExitPages: ExitPage[];
|
|
197
|
+
/** Always true. The UI must not present these steps as a tracked journey. */
|
|
198
|
+
approximate: true;
|
|
199
|
+
/** Why it is approximate, in words the panel can show directly. */
|
|
200
|
+
approximationNote: string;
|
|
201
|
+
}
|
|
202
|
+
/** One family's interest figures. */
|
|
203
|
+
interface TypefaceInterestRow {
|
|
204
|
+
typeface: string;
|
|
205
|
+
viewed: MetricValue;
|
|
206
|
+
tested: MetricValue;
|
|
207
|
+
bought: MetricValue;
|
|
208
|
+
/** Tested divided by viewed. Null unless both are real numbers. */
|
|
209
|
+
testRate: number | null;
|
|
210
|
+
}
|
|
211
|
+
/** Viewed, tested and bought, by family. */
|
|
212
|
+
interface TypefaceInterestData {
|
|
213
|
+
rows: TypefaceInterestRow[];
|
|
214
|
+
/** Stated in the response so the UI cannot omit it. */
|
|
215
|
+
interpretationNote: string;
|
|
216
|
+
/** True when GA4 withheld low-count rows, so quiet families may be missing entirely. */
|
|
217
|
+
rowsWithheld: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Acquisition — where visitors come from.
|
|
222
|
+
*
|
|
223
|
+
* Design-industry referrers are pulled out as a named segment rather than left in a generic
|
|
224
|
+
* referrer table. A visit from Fonts In Use or Typewolf is a pre-qualified, industry-literate
|
|
225
|
+
* visitor and behaves nothing like average organic traffic; burying those rows among search and
|
|
226
|
+
* social discards the distinction a foundry actually acts on.
|
|
227
|
+
*
|
|
228
|
+
* `(not set)` rows are surfaced rather than swept into "other". On a type-foundry site they can be
|
|
229
|
+
* a large share — bookmarked visits, stripped referrers, AI crawlers — and hiding them makes the
|
|
230
|
+
* table look more complete than it is.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/** Referrer hosts that identify a design-industry source worth tracking separately. */
|
|
234
|
+
declare const DESIGN_INDUSTRY_SOURCES: readonly ["fontsinuse.com", "typewolf.com", "typographica.org", "fonts.google.com", "behance.net", "dribbble.com", "itsnicethat.com"];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Journey — how far visitors get, and where they stop.
|
|
238
|
+
*
|
|
239
|
+
* This is an ordered step funnel, not a path graph, and that is a deliberate limit rather than a
|
|
240
|
+
* simplification. GA4's Data API has no path-exploration endpoint; Path Exploration is a UI-only
|
|
241
|
+
* feature. What is available is per-event totals and `runFunnelReport`, an alpha surface that
|
|
242
|
+
* returns step-conversion marginals — not observed sequences. Rendering ribbons from marginals
|
|
243
|
+
* would assert that a given visitor went A to B to C when no such co-occurrence was ever measured.
|
|
244
|
+
*
|
|
245
|
+
* So each step is reported as its own honest total, adjacent drop-off is derived between steps, and
|
|
246
|
+
* the response is explicitly flagged as an approximation for the UI to display.
|
|
247
|
+
*
|
|
248
|
+
* A step whose event is not instrumented on this site reports as unavailable, never as zero. On a
|
|
249
|
+
* site missing `begin_checkout`, the cart-to-checkout drop-off is not "100% drop-off" — it is
|
|
250
|
+
* unmeasured, and the two must not look alike.
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
/** The funnel, in order. Each entry names the GA4 event that evidences the step. */
|
|
254
|
+
declare const JOURNEY_STEPS: readonly [{
|
|
255
|
+
readonly key: "landed";
|
|
256
|
+
readonly label: "Landed";
|
|
257
|
+
readonly event: "page_view";
|
|
258
|
+
}, {
|
|
259
|
+
readonly key: "viewed_typeface";
|
|
260
|
+
readonly label: "Viewed a typeface";
|
|
261
|
+
readonly event: "view_item";
|
|
262
|
+
}, {
|
|
263
|
+
readonly key: "tested";
|
|
264
|
+
readonly label: "Used the type tester";
|
|
265
|
+
readonly event: "tester_engaged";
|
|
266
|
+
}, {
|
|
267
|
+
readonly key: "added_to_cart";
|
|
268
|
+
readonly label: "Added to cart";
|
|
269
|
+
readonly event: "add_to_cart";
|
|
270
|
+
}, {
|
|
271
|
+
readonly key: "began_checkout";
|
|
272
|
+
readonly label: "Began checkout";
|
|
273
|
+
readonly event: "begin_checkout";
|
|
274
|
+
}, {
|
|
275
|
+
readonly key: "purchased";
|
|
276
|
+
readonly label: "Purchased";
|
|
277
|
+
readonly event: "purchase";
|
|
278
|
+
}];
|
|
279
|
+
|
|
280
|
+
export { type AcquisitionData, DESIGN_INDUSTRY_SOURCES, ENV_VARS, type ExitPage, type HandlerOptions, type HandlerRequest, type HandlerResponse, JOURNEY_STEPS, type JourneyData, type JourneyStep, type MeasurementHealthData, MetricValue, type SanityQueryClient, SiteAnalyticsConfig, type SourceRow, type StudioUser, type TypefaceInterestData, type TypefaceInterestRow, applyCors, clearCache, createVisitorInsightsHandler, requireStudioUser, verifyStudioRequest };
|