@sceneinfrastructure/storefront-analytics 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 ADDED
@@ -0,0 +1,114 @@
1
+ # @sceneinfrastructure/storefront-analytics
2
+
3
+ Browser/server attribution helpers and shared schemas for Mesh
4
+ storefronts. One publishable `pk_` token in, everything else — visit
5
+ cookie, beacons, handoff decoration — handled inside.
6
+
7
+ ```bash
8
+ pnpm add @sceneinfrastructure/storefront-analytics
9
+ ```
10
+
11
+ ## Tier 1 — base install
12
+
13
+ Next.js, one component in the root layout:
14
+
15
+ ```tsx
16
+ import { MeshAnalytics } from '@sceneinfrastructure/storefront-analytics/react'
17
+
18
+ export default function RootLayout({ children }) {
19
+ return (
20
+ <html>
21
+ <body>
22
+ {children}
23
+ <MeshAnalytics
24
+ token={process.env.NEXT_PUBLIC_MESH_ANALYTICS_TOKEN!}
25
+ apiBase={process.env.NEXT_PUBLIC_MESH_API_BASE_URL!}
26
+ meshHosts={['checkout.mesh.ing']}
27
+ />
28
+ </body>
29
+ </html>
30
+ )
31
+ }
32
+ ```
33
+
34
+ No Next.js? Same behavior as a tag (point `src` at your pinned version):
35
+
36
+ ```html
37
+ <script
38
+ src="https://cdn.mesh.ing/v1/m.js"
39
+ data-token="pk_live_..."
40
+ data-api-base="https://api.mesh.ing"
41
+ data-mesh-hosts="checkout.mesh.ing"
42
+ ></script>
43
+ <script>mesh.track('cart.updated', { lines: [...] })</script>
44
+ ```
45
+
46
+ The token is the publishable `pk_` key: scene-scoped and origin-pinned,
47
+ safe for browsers. Never use an `sk_` key here.
48
+
49
+ ## Tier 2 — cart tracking
50
+
51
+ Wherever quantity state already lives (Client Components only for the
52
+ hook):
53
+
54
+ ```tsx
55
+ 'use client'
56
+
57
+ import { useStorefrontAnalytics } from '@sceneinfrastructure/storefront-analytics/react'
58
+
59
+ const { track } = useStorefrontAnalytics({
60
+ token: process.env.NEXT_PUBLIC_MESH_ANALYTICS_TOKEN!,
61
+ apiBase: process.env.NEXT_PUBLIC_MESH_API_BASE_URL!,
62
+ })
63
+
64
+ track('cart.updated', {
65
+ eventId,
66
+ lines: [{ saleKeyId, quantity }],
67
+ })
68
+ ```
69
+
70
+ Use **either** `<MeshAnalytics/>` **or** the hook per app, never both —
71
+ the hook binds a tag-provided `window.mesh` when present so mixed setups
72
+ never double-track.
73
+
74
+ ## Tier 3 — join to purchase
75
+
76
+ In the backend, wherever `createCartUrl` is already called:
77
+
78
+ ```ts
79
+ import { getAttribution } from '@sceneinfrastructure/storefront-analytics/server'
80
+
81
+ const { visitId } = getAttribution(req)
82
+ await mesh.createCartUrl({ eventId, items, attribution: { visitId } })
83
+ ```
84
+
85
+ Server-emitted events (webhooks, backend cart updates) go through the
86
+ secret route:
87
+
88
+ ```ts
89
+ import { trackServerEvent } from '@sceneinfrastructure/storefront-analytics/server'
90
+
91
+ await trackServerEvent({
92
+ apiBase: process.env.MESH_PUBLIC_API_BASE_URL!,
93
+ apiKey: process.env.MESH_SCENE_API_KEY!, // sk_ — server-only
94
+ event: { eventName: 'order.confirmed', orderId },
95
+ })
96
+ ```
97
+
98
+ Non-TypeScript backends use the same field names over HTTP
99
+ (`POST /v1/analytics/track`, `POST /v1/cart-url` with `attribution`).
100
+
101
+ ## Consent
102
+
103
+ Consent is integrator-managed. Wire the CMP state into `consent`
104
+ (component prop, hook config, or `data-consent="false"` on the tag).
105
+ `false` disables minting, storage, decoration, and beacons. Default is
106
+ tracking-enabled — regional opt-in gating is the integrator's call.
107
+
108
+ ## Subpaths
109
+
110
+ - `@sceneinfrastructure/storefront-analytics` — schemas (safe anywhere).
111
+ - `.../client` — vanilla browser core (zero dependencies).
112
+ - `.../react` — `<MeshAnalytics/>` + `useStorefrontAnalytics`.
113
+ - `.../server` — Node-only helpers (`server-only` unsafe for browsers).
114
+ - `.../schemas` — shared zod primitives (imported by the API contracts).
@@ -0,0 +1,233 @@
1
+ // src/client.ts
2
+ var MESH_VISIT_COOKIE = "mesh_visit";
3
+ var MESH_VISIT_COOKIE_MAX_AGE_SECONDS = 90 * 24 * 60 * 60;
4
+ var HANDOFF_REFRESH_BEFORE_EXPIRY_MS = 5 * 60 * 1e3;
5
+ var HANDOFF_TOKEN_TTL_MS = 10 * 60 * 1e3;
6
+ function defaultPlatform(cookieDomain) {
7
+ return {
8
+ fetchFn: window.fetch.bind(window),
9
+ locationHref: typeof window.location.href === "string" ? window.location.href : "",
10
+ navigate: (href, target) => {
11
+ if (target === "_blank") {
12
+ window.open(href, "_blank", "noopener");
13
+ return;
14
+ }
15
+ window.location.assign(href);
16
+ },
17
+ now: () => Date.now(),
18
+ randomId: () => typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
19
+ readCookie: (name) => {
20
+ const match = document.cookie.match(
21
+ new RegExp(`(?:^|;\\s*)${name}=([^;]*)`)
22
+ );
23
+ return match?.[1] ? decodeURIComponent(match[1]) : void 0;
24
+ },
25
+ sendBeacon: typeof navigator.sendBeacon === "function" ? navigator.sendBeacon.bind(navigator) : void 0,
26
+ writeCookie: (name, value, maxAgeSeconds) => {
27
+ const domain = cookieDomain ? `; Domain=${cookieDomain}` : "";
28
+ document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${domain}` + (window.location.protocol === "https:" ? "; Secure" : "");
29
+ }
30
+ };
31
+ }
32
+ function apiUrl(apiBase, path) {
33
+ return `${apiBase.replace(/\/+$/, "")}/v1${path}`;
34
+ }
35
+ function hostMatches(hostname, patterns) {
36
+ const host = hostname.toLowerCase();
37
+ return patterns.some((pattern) => {
38
+ const candidate = pattern.toLowerCase().replace(/^\./, "");
39
+ return host === candidate || host.endsWith(`.${candidate}`);
40
+ });
41
+ }
42
+ function clickAnchor(target) {
43
+ const element = target;
44
+ const anchor = element?.closest?.("a[href]") ?? null;
45
+ const href = typeof anchor?.getAttribute === "function" ? anchor.getAttribute("href") : null;
46
+ if (!href) return null;
47
+ return {
48
+ href,
49
+ target: typeof anchor?.getAttribute === "function" ? anchor.getAttribute("target") ?? null : null
50
+ };
51
+ }
52
+ function createStorefrontAnalyticsInstance(config, platform) {
53
+ let consent = config.consent ?? true;
54
+ const meshHosts = config.meshHosts ?? [];
55
+ let visitId = consent ? platform.readCookie(MESH_VISIT_COOKIE) : void 0;
56
+ let handoff;
57
+ function headers() {
58
+ return {
59
+ Authorization: `Bearer ${config.token}`,
60
+ "Content-Type": "application/json"
61
+ };
62
+ }
63
+ async function postTrack(eventName, props, needsResponse) {
64
+ const effectiveVisitId = props.visitId ?? visitId;
65
+ const body = JSON.stringify({
66
+ eventId: props.eventId ?? platform.randomId(),
67
+ eventName,
68
+ ...props.cartId ? { cartId: props.cartId } : {},
69
+ ...props.cartRevision !== void 0 ? { cartRevision: props.cartRevision } : {},
70
+ ...props.eventContractId ? { eventContractId: props.eventContractId } : {},
71
+ ...props.lines ? { lines: props.lines } : {},
72
+ ...props.referrer ? { referrer: props.referrer } : {},
73
+ ...props.saleKeyId ? { saleKeyId: props.saleKeyId } : {},
74
+ ...props.url ? { url: props.url } : {},
75
+ ...props.utm ? { utm: props.utm } : {},
76
+ ...effectiveVisitId ? { visitId: effectiveVisitId } : {}
77
+ });
78
+ const url = apiUrl(config.apiBase, "/analytics/track");
79
+ const response = await platform.fetchFn(url, {
80
+ body,
81
+ headers: headers(),
82
+ keepalive: true,
83
+ method: "POST"
84
+ });
85
+ if (!response.ok) return void 0;
86
+ return await response.json();
87
+ }
88
+ async function track(eventName, props = {}) {
89
+ if (!consent) return void 0;
90
+ const response = await postTrack(eventName, props);
91
+ if (response?.visitId && response.visitId !== visitId) {
92
+ visitId = response.visitId;
93
+ platform.writeCookie(
94
+ MESH_VISIT_COOKIE,
95
+ visitId,
96
+ MESH_VISIT_COOKIE_MAX_AGE_SECONDS
97
+ );
98
+ }
99
+ return response;
100
+ }
101
+ async function mintHandoff() {
102
+ if (!consent || !visitId) return void 0;
103
+ const now = platform.now();
104
+ if (handoff && handoff.exp - now > HANDOFF_REFRESH_BEFORE_EXPIRY_MS) {
105
+ return handoff.token;
106
+ }
107
+ try {
108
+ const response = await platform.fetchFn(
109
+ apiUrl(config.apiBase, "/analytics/handoff"),
110
+ {
111
+ body: JSON.stringify({ visitId }),
112
+ headers: headers(),
113
+ method: "POST"
114
+ }
115
+ );
116
+ if (!response.ok) return void 0;
117
+ const payload = await response.json();
118
+ if (!payload.token) return void 0;
119
+ handoff = { exp: now + HANDOFF_TOKEN_TTL_MS, token: payload.token };
120
+ return payload.token;
121
+ } catch {
122
+ return void 0;
123
+ }
124
+ }
125
+ async function decorateUrl(url) {
126
+ if (!consent || meshHosts.length === 0) return url;
127
+ let parsed;
128
+ try {
129
+ parsed = new URL(url, platform.locationHref);
130
+ } catch {
131
+ return url;
132
+ }
133
+ if (!hostMatches(parsed.hostname, meshHosts)) return url;
134
+ const token = await mintHandoff();
135
+ if (!token) return url;
136
+ const exchange = new URL("/handoff", `${parsed.protocol}//${parsed.host}`);
137
+ exchange.searchParams.set("token", token);
138
+ exchange.searchParams.set("dest", `${parsed.pathname}${parsed.search}`);
139
+ return exchange.toString();
140
+ }
141
+ function interceptLinks(root) {
142
+ const onClick = (event) => {
143
+ const native = event;
144
+ if (native.defaultPrevented || native.metaKey || native.ctrlKey || native.shiftKey || native.altKey) {
145
+ return;
146
+ }
147
+ const hit = clickAnchor(native.target);
148
+ if (!hit) return;
149
+ let parsed;
150
+ try {
151
+ parsed = new URL(hit.href, platform.locationHref);
152
+ } catch {
153
+ return;
154
+ }
155
+ if (!consent || !hostMatches(parsed.hostname, meshHosts)) return;
156
+ native.preventDefault?.();
157
+ void (async () => {
158
+ platform.navigate(await decorateUrl(hit.href), hit.target);
159
+ })();
160
+ };
161
+ root.addEventListener("click", onClick, true);
162
+ return () => {
163
+ root.removeEventListener("click", onClick, true);
164
+ };
165
+ }
166
+ function reset() {
167
+ visitId = void 0;
168
+ handoff = void 0;
169
+ }
170
+ function setConsent(next) {
171
+ consent = next;
172
+ if (!next) reset();
173
+ }
174
+ const detachers = /* @__PURE__ */ new Set();
175
+ function dispose() {
176
+ for (const detach of detachers) detach();
177
+ detachers.clear();
178
+ for (const [key, candidate] of instances) {
179
+ if (candidate === api) instances.delete(key);
180
+ }
181
+ }
182
+ function interceptingLinks(root) {
183
+ const detach = interceptLinks(root);
184
+ detachers.add(detach);
185
+ return () => {
186
+ detach();
187
+ detachers.delete(detach);
188
+ };
189
+ }
190
+ const api = {
191
+ decorateUrl,
192
+ dispose,
193
+ getVisitId: () => visitId,
194
+ interceptLinks: interceptingLinks,
195
+ reset,
196
+ setConsent,
197
+ track
198
+ };
199
+ return api;
200
+ }
201
+ var instances = /* @__PURE__ */ new Map();
202
+ function initStorefrontAnalytics(config, platform) {
203
+ const key = [
204
+ config.apiBase,
205
+ config.token,
206
+ (config.meshHosts ?? []).join(","),
207
+ config.cookieDomain ?? "",
208
+ config.consent ?? true
209
+ ].join("::");
210
+ const existing = instances.get(key);
211
+ if (existing) return existing;
212
+ const resolved = platform ?? (typeof document === "undefined" ? void 0 : defaultPlatform(config.cookieDomain));
213
+ if (!resolved) {
214
+ throw new Error(
215
+ "initStorefrontAnalytics requires a DOM platform outside browsers."
216
+ );
217
+ }
218
+ const instance = createStorefrontAnalyticsInstance(config, resolved);
219
+ instances.set(key, instance);
220
+ if (typeof document !== "undefined") {
221
+ instance.interceptLinks(document);
222
+ void instance.track("visit.started", {
223
+ referrer: document.referrer || void 0,
224
+ url: resolved.locationHref || void 0
225
+ });
226
+ }
227
+ return instance;
228
+ }
229
+ function resetStorefrontAnalyticsInstances() {
230
+ instances.clear();
231
+ }
232
+
233
+ export { MESH_VISIT_COOKIE, createStorefrontAnalyticsInstance, defaultPlatform, initStorefrontAnalytics, resetStorefrontAnalyticsInstances };
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/schemas.ts
4
+ var storefrontAnalyticsEvents = {
5
+ cartUpdated: "cart.updated",
6
+ cartUrlCreated: "cart.url_created",
7
+ checkoutStarted: "checkout.started",
8
+ eventView: "event.viewed",
9
+ orderConfirmed: "order.confirmed",
10
+ orderExpired: "order.expired",
11
+ orderRefunded: "order.refunded",
12
+ orderReserved: "order.reserved",
13
+ orderVoided: "order.voided",
14
+ tierView: "tier.viewed",
15
+ visit: "visit.started"
16
+ };
17
+ var storefrontAnalyticsEventNameSchema = z.enum([
18
+ storefrontAnalyticsEvents.visit,
19
+ storefrontAnalyticsEvents.eventView,
20
+ storefrontAnalyticsEvents.tierView,
21
+ storefrontAnalyticsEvents.cartUpdated,
22
+ storefrontAnalyticsEvents.cartUrlCreated,
23
+ storefrontAnalyticsEvents.checkoutStarted,
24
+ storefrontAnalyticsEvents.orderReserved,
25
+ storefrontAnalyticsEvents.orderConfirmed,
26
+ storefrontAnalyticsEvents.orderExpired,
27
+ storefrontAnalyticsEvents.orderVoided,
28
+ storefrontAnalyticsEvents.orderRefunded
29
+ ]);
30
+ var uuid = z.string().uuid();
31
+ var hasNoControlCharacters = (value) => [...value].every((char) => {
32
+ const code = char.codePointAt(0) ?? 0;
33
+ return code >= 32 && code !== 127;
34
+ });
35
+ var trimmed = (max) => z.string().trim().min(1).max(max).refine(hasNoControlCharacters, {
36
+ message: "must not contain control characters"
37
+ });
38
+ var optionalTrimmed = (max) => z.string().trim().max(max).refine(hasNoControlCharacters, {
39
+ message: "must not contain control characters"
40
+ }).transform((value) => value === "" ? void 0 : value).optional();
41
+ var analyticsUtmSchema = z.object({
42
+ utm_campaign: optionalTrimmed(512),
43
+ utm_content: optionalTrimmed(512),
44
+ utm_id: optionalTrimmed(512),
45
+ utm_medium: optionalTrimmed(512),
46
+ utm_source: optionalTrimmed(512),
47
+ utm_term: optionalTrimmed(512)
48
+ });
49
+ var analyticsConsentStateSchema = z.enum([
50
+ "granted",
51
+ "denied",
52
+ "unknown"
53
+ ]);
54
+ var storefrontAttributionSchema = z.object({
55
+ cartId: uuid.optional(),
56
+ cartRevision: z.number().int().nonnegative().optional(),
57
+ consentState: analyticsConsentStateSchema.optional(),
58
+ utm: analyticsUtmSchema.optional(),
59
+ visitId: uuid
60
+ });
61
+ var saleKeyIdSchema = z.string().regex(/^0x[0-9a-fA-F]+$/);
62
+ var analyticsLineSchema = z.object({
63
+ quantity: z.number().int().min(1),
64
+ saleKeyId: saleKeyIdSchema
65
+ });
66
+ var serverAnalyticsLineSchema = analyticsLineSchema.extend({
67
+ amountUsdc: z.string().regex(/^-?\d+$/, "must be an integer base-unit string").optional(),
68
+ refundId: uuid.optional()
69
+ });
70
+ var browserEventBaseSchema = z.object({
71
+ cartId: uuid.optional(),
72
+ cartRevision: z.number().int().nonnegative().optional(),
73
+ eventId: uuid,
74
+ referrer: z.string().trim().max(2048).optional(),
75
+ url: z.string().trim().max(2048).optional(),
76
+ utm: analyticsUtmSchema.optional(),
77
+ visitId: uuid.optional()
78
+ });
79
+ var browserAnalyticsEventSchema = z.discriminatedUnion("eventName", [
80
+ browserEventBaseSchema.extend({
81
+ eventName: z.literal(storefrontAnalyticsEvents.visit)
82
+ }),
83
+ browserEventBaseSchema.extend({
84
+ eventContractId: trimmed(64),
85
+ eventName: z.literal(storefrontAnalyticsEvents.eventView)
86
+ }),
87
+ browserEventBaseSchema.extend({
88
+ eventName: z.literal(storefrontAnalyticsEvents.tierView),
89
+ saleKeyId: saleKeyIdSchema
90
+ }),
91
+ browserEventBaseSchema.extend({
92
+ cartId: uuid,
93
+ cartRevision: z.number().int().nonnegative(),
94
+ eventName: z.literal(storefrontAnalyticsEvents.cartUpdated),
95
+ lines: z.array(analyticsLineSchema).min(1).max(50)
96
+ })
97
+ ]);
98
+ var trackAnalyticsEventInputSchema = z.object({
99
+ cartId: uuid.optional(),
100
+ cartRevision: z.number().int().nonnegative().optional(),
101
+ eventContractId: trimmed(64).optional(),
102
+ eventId: uuid,
103
+ eventName: storefrontAnalyticsEventNameSchema,
104
+ lines: z.array(analyticsLineSchema).max(50).optional(),
105
+ referrer: z.string().trim().max(2048).optional(),
106
+ saleKeyId: saleKeyIdSchema.optional(),
107
+ sceneId: trimmed(64).optional(),
108
+ url: z.string().trim().max(2048).optional(),
109
+ utm: analyticsUtmSchema.optional(),
110
+ visitId: uuid.optional()
111
+ });
112
+ var createHandoffTokenInputSchema = z.object({
113
+ visitId: uuid
114
+ });
115
+ var createHandoffTokenOutputSchema = z.object({
116
+ token: z.string().min(1)
117
+ });
118
+
119
+ export { analyticsConsentStateSchema, analyticsUtmSchema, browserAnalyticsEventSchema, createHandoffTokenInputSchema, createHandoffTokenOutputSchema, serverAnalyticsLineSchema, storefrontAnalyticsEventNameSchema, storefrontAnalyticsEvents, storefrontAttributionSchema, trackAnalyticsEventInputSchema };
@@ -0,0 +1,67 @@
1
+ type StorefrontAnalyticsCartLine = {
2
+ quantity: number;
3
+ saleKeyId: string;
4
+ };
5
+ type StorefrontAnalyticsTrackProps = {
6
+ cartId?: string;
7
+ cartRevision?: number;
8
+ eventContractId?: string;
9
+ eventId?: string;
10
+ lines?: Array<StorefrontAnalyticsCartLine>;
11
+ referrer?: string;
12
+ saleKeyId?: string;
13
+ url?: string;
14
+ utm?: Record<string, string | undefined>;
15
+ visitId?: string;
16
+ };
17
+ type StorefrontAnalyticsConfig = {
18
+ /** Publishable `pk_` key. Scene-scoped and origin-pinned. */
19
+ token: string;
20
+ /** Mesh API origin, e.g. `https://api.mesh.ing` (no `/v1` suffix). */
21
+ apiBase: string;
22
+ /** Hosted Mesh domains whose links get handoff decoration. */
23
+ meshHosts?: Array<string>;
24
+ /** Cookie domain override. Defaults to the current hostname. */
25
+ cookieDomain?: string;
26
+ /** Integrator CMP wiring. `false` disables mint, storage, beacons. */
27
+ consent?: boolean;
28
+ };
29
+ type StorefrontAnalyticsTrackResponse = {
30
+ accepted: boolean;
31
+ visitId: string;
32
+ };
33
+ type EventTargetLike = {
34
+ addEventListener(type: string, listener: (event: unknown) => void, options?: boolean | AddEventListenerOptions): void;
35
+ removeEventListener(type: string, listener: (event: unknown) => void, options?: boolean | AddEventListenerOptions): void;
36
+ };
37
+ /** Seams for unit tests; defaults bind browser globals. */
38
+ type StorefrontAnalyticsPlatform = {
39
+ fetchFn: typeof fetch;
40
+ locationHref: string;
41
+ navigate: (href: string, target?: string | null) => void;
42
+ now: () => number;
43
+ randomId: () => string;
44
+ readCookie: (name: string) => string | undefined;
45
+ sendBeacon?: (url: string, body: Blob) => boolean;
46
+ writeCookie: (name: string, value: string, maxAgeSeconds: number) => void;
47
+ };
48
+ declare const MESH_VISIT_COOKIE = "mesh_visit";
49
+ declare function defaultPlatform(cookieDomain?: string): StorefrontAnalyticsPlatform;
50
+ type StorefrontAnalyticsInstance = {
51
+ track: (eventName: string, props?: StorefrontAnalyticsTrackProps) => Promise<StorefrontAnalyticsTrackResponse | undefined>;
52
+ getVisitId: () => string | undefined;
53
+ decorateUrl: (url: string) => Promise<string>;
54
+ interceptLinks: (root: EventTargetLike) => () => void;
55
+ reset: () => void;
56
+ /** Flip consent on the live instance (CMP flows). `false` also drops
57
+ * any minted visit, so revoked consent stops tracking immediately. */
58
+ setConsent: (next: boolean) => void;
59
+ /** Detach listeners and drop memoization. Call on provider unmount. */
60
+ dispose: () => void;
61
+ };
62
+ declare function createStorefrontAnalyticsInstance(config: StorefrontAnalyticsConfig, platform: StorefrontAnalyticsPlatform): StorefrontAnalyticsInstance;
63
+ declare function initStorefrontAnalytics(config: StorefrontAnalyticsConfig, platform?: StorefrontAnalyticsPlatform): StorefrontAnalyticsInstance;
64
+ /** Test-only: drop memoized singletons between cases. */
65
+ declare function resetStorefrontAnalyticsInstances(): void;
66
+
67
+ export { type EventTargetLike, MESH_VISIT_COOKIE, type StorefrontAnalyticsCartLine, type StorefrontAnalyticsConfig, type StorefrontAnalyticsInstance, type StorefrontAnalyticsPlatform, type StorefrontAnalyticsTrackProps, type StorefrontAnalyticsTrackResponse, createStorefrontAnalyticsInstance, defaultPlatform, initStorefrontAnalytics, resetStorefrontAnalyticsInstances };
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ export { MESH_VISIT_COOKIE, createStorefrontAnalyticsInstance, defaultPlatform, initStorefrontAnalytics, resetStorefrontAnalyticsInstances } from './chunk-6XI65T7B.js';
@@ -0,0 +1,2 @@
1
+ export { AnalyticsConsentState, AnalyticsUtm, BrowserAnalyticsEvent, CreateHandoffTokenInput, CreateHandoffTokenOutput, ServerAnalyticsLine, StorefrontAnalyticsEventName, StorefrontAttribution, TrackAnalyticsEventInput, analyticsConsentStateSchema, analyticsUtmSchema, browserAnalyticsEventSchema, createHandoffTokenInputSchema, createHandoffTokenOutputSchema, serverAnalyticsLineSchema, storefrontAnalyticsEventNameSchema, storefrontAnalyticsEvents, storefrontAttributionSchema, trackAnalyticsEventInputSchema } from './schemas.js';
2
+ import 'zod';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { analyticsConsentStateSchema, analyticsUtmSchema, browserAnalyticsEventSchema, createHandoffTokenInputSchema, createHandoffTokenOutputSchema, serverAnalyticsLineSchema, storefrontAnalyticsEventNameSchema, storefrontAnalyticsEvents, storefrontAttributionSchema, trackAnalyticsEventInputSchema } from './chunk-NTTEMMBO.js';
package/dist/m.js ADDED
@@ -0,0 +1,2 @@
1
+ var meshAnalyticsBundle=(function(exports){'use strict';var k="mesh_visit";function P(t){return {fetchFn:window.fetch.bind(window),locationHref:typeof window.location.href=="string"?window.location.href:"",navigate:(n,r)=>{if(r==="_blank"){window.open(n,"_blank","noopener");return}window.location.assign(n);},now:()=>Date.now(),randomId:()=>typeof crypto.randomUUID=="function"?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,readCookie:n=>{let r=document.cookie.match(new RegExp(`(?:^|;\\s*)${n}=([^;]*)`));return r?.[1]?decodeURIComponent(r[1]):void 0},sendBeacon:typeof navigator.sendBeacon=="function"?navigator.sendBeacon.bind(navigator):void 0,writeCookie:(n,r,c)=>{let i=t?`; Domain=${t}`:"";document.cookie=`${n}=${encodeURIComponent(r)}; Path=/; Max-Age=${c}; SameSite=Lax${i}`+(window.location.protocol==="https:"?"; Secure":"");}}}function S(t,n){return `${t.replace(/\/+$/,"")}/v1${n}`}function w(t,n){let r=t.toLowerCase();return n.some(c=>{let i=c.toLowerCase().replace(/^\./,"");return r===i||r.endsWith(`.${i}`)})}function x(t){let r=t?.closest?.("a[href]")??null,c=typeof r?.getAttribute=="function"?r.getAttribute("href"):null;return c?{href:c,target:typeof r?.getAttribute=="function"?r.getAttribute("target")??null:null}:null}function K(t,n){let r=t.consent??true,c=t.meshHosts??[],i=r?n.readCookie(k):void 0,d;function m(){return {Authorization:`Bearer ${t.token}`,"Content-Type":"application/json"}}async function C(o,e,s){let a=e.visitId??i,f=JSON.stringify({eventId:e.eventId??n.randomId(),eventName:o,...e.cartId?{cartId:e.cartId}:{},...e.cartRevision!==void 0?{cartRevision:e.cartRevision}:{},...e.eventContractId?{eventContractId:e.eventContractId}:{},...e.lines?{lines:e.lines}:{},...e.referrer?{referrer:e.referrer}:{},...e.saleKeyId?{saleKeyId:e.saleKeyId}:{},...e.url?{url:e.url}:{},...e.utm?{utm:e.utm}:{},...a?{visitId:a}:{}}),u=S(t.apiBase,"/analytics/track");let I=await n.fetchFn(u,{body:f,headers:m(),keepalive:true,method:"POST"});if(I.ok)return await I.json()}async function _(o,e={}){if(!r)return;let s=await C(o,e);return s?.visitId&&s.visitId!==i&&(i=s.visitId,n.writeCookie(k,i,7776e3)),s}async function L(){if(!r||!i)return;let o=n.now();if(d&&d.exp-o>3e5)return d.token;try{let e=await n.fetchFn(S(t.apiBase,"/analytics/handoff"),{body:JSON.stringify({visitId:i}),headers:m(),method:"POST"});if(!e.ok)return;let s=await e.json();return s.token?(d={exp:o+6e5,token:s.token},s.token):void 0}catch{return}}async function v(o){if(!r||c.length===0)return o;let e;try{e=new URL(o,n.locationHref);}catch{return o}if(!w(e.hostname,c))return o;let s=await L();if(!s)return o;let a=new URL("/handoff",`${e.protocol}//${e.host}`);return a.searchParams.set("token",s),a.searchParams.set("dest",`${e.pathname}${e.search}`),a.toString()}function T(o){let e=s=>{let a=s;if(a.defaultPrevented||a.metaKey||a.ctrlKey||a.shiftKey||a.altKey)return;let f=x(a.target);if(!f)return;let u;try{u=new URL(f.href,n.locationHref);}catch{return}!r||!w(u.hostname,c)||(a.preventDefault?.(),(async()=>n.navigate(await v(f.href),f.target))());};return o.addEventListener("click",e,true),()=>{o.removeEventListener("click",e,true);}}function h(){i=void 0,d=void 0;}function H(o){r=o,o||h();}let l=new Set;function O(){for(let o of l)o();l.clear();for(let[o,e]of g)e===p&&g.delete(o);}function R(o){let e=T(o);return l.add(e),()=>{e(),l.delete(e);}}let p={decorateUrl:v,dispose:O,getVisitId:()=>i,interceptLinks:R,reset:h,setConsent:H,track:_};return p}var g=new Map;function A(t,n){let r=[t.apiBase,t.token,(t.meshHosts??[]).join(","),t.cookieDomain??"",t.consent??true].join("::"),c=g.get(r);if(c)return c;let i=(typeof document>"u"?void 0:P(t.cookieDomain));if(!i)throw new Error("initStorefrontAnalytics requires a DOM platform outside browsers.");let d=K(t,i);return g.set(r,d),typeof document<"u"&&(d.interceptLinks(document),d.track("visit.started",{referrer:document.referrer||void 0,url:i.locationHref||void 0})),d}function y(t){return document.currentScript?.dataset[t]}var E=y("token")??"",b=y("apiBase")??"",B=(y("meshHosts")??"").split(",").map(t=>t.trim()).filter(t=>t.length>0),U=y("consent")!=="false";if(E&&b){let t=A({apiBase:b,consent:U,meshHosts:B,token:E});window.mesh={decorateUrl:n=>t.decorateUrl(n),getVisitId:()=>t.getVisitId(),track:(n,r={})=>t.track(n,r)};}
2
+ exports.MESH_VISIT_COOKIE=k;return exports;})({});
@@ -0,0 +1,16 @@
1
+ import { ReactElement } from 'react';
2
+ import { StorefrontAnalyticsConfig, StorefrontAnalyticsInstance } from './client.js';
3
+
4
+ declare const MESH_CDN_URL = "https://cdn.mesh.ing/v1/m.js";
5
+ declare function MeshAnalytics({ apiBase, cdnUrl, consent, cookieDomain, meshHosts, token, }: StorefrontAnalyticsConfig & {
6
+ cdnUrl?: string;
7
+ }): ReactElement;
8
+ type UseStorefrontAnalytics = {
9
+ decorateUrl: (url: string) => Promise<string>;
10
+ getVisitId: () => string | undefined;
11
+ ready: boolean;
12
+ track: StorefrontAnalyticsInstance['track'];
13
+ };
14
+ declare function useStorefrontAnalytics(config: StorefrontAnalyticsConfig): UseStorefrontAnalytics;
15
+
16
+ export { MESH_CDN_URL, MeshAnalytics, type UseStorefrontAnalytics, useStorefrontAnalytics };
package/dist/react.js ADDED
@@ -0,0 +1,77 @@
1
+ import { initStorefrontAnalytics } from './chunk-6XI65T7B.js';
2
+ import Script from 'next/script';
3
+ import { useMemo, useEffect } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ var MESH_CDN_URL = "https://cdn.mesh.ing/v1/m.js";
7
+ function MeshAnalytics({
8
+ apiBase,
9
+ cdnUrl = MESH_CDN_URL,
10
+ consent = true,
11
+ cookieDomain,
12
+ meshHosts = [],
13
+ token
14
+ }) {
15
+ return /* @__PURE__ */ jsx(
16
+ Script,
17
+ {
18
+ "data-api-base": apiBase,
19
+ "data-consent": consent ? void 0 : "false",
20
+ "data-cookie-domain": cookieDomain,
21
+ "data-mesh-hosts": meshHosts.join(","),
22
+ "data-token": token,
23
+ src: cdnUrl,
24
+ strategy: "afterInteractive"
25
+ }
26
+ );
27
+ }
28
+ function windowMesh() {
29
+ if (typeof window === "undefined") return void 0;
30
+ const candidate = window.mesh;
31
+ if (!candidate || typeof candidate !== "object") return void 0;
32
+ const mesh = candidate;
33
+ if (typeof mesh.track !== "function" || typeof mesh.getVisitId !== "function" || typeof mesh.decorateUrl !== "function") {
34
+ return void 0;
35
+ }
36
+ return mesh;
37
+ }
38
+ function useStorefrontAnalytics(config) {
39
+ const { apiBase, consent, cookieDomain, meshHosts, token } = config;
40
+ const meshHostsKey = (meshHosts ?? []).join(",");
41
+ const instance = useMemo(() => {
42
+ if (typeof window === "undefined") {
43
+ throw new Error("useStorefrontAnalytics requires a browser.");
44
+ }
45
+ const cdn = windowMesh();
46
+ if (cdn) return cdn;
47
+ return initStorefrontAnalytics({
48
+ apiBase,
49
+ consent,
50
+ cookieDomain,
51
+ meshHosts: meshHostsKey.length > 0 ? meshHostsKey.split(",") : [],
52
+ token
53
+ });
54
+ }, [apiBase, consent, cookieDomain, meshHostsKey, token]);
55
+ useEffect(() => {
56
+ instance.setConsent(consent ?? true);
57
+ return () => {
58
+ instance.dispose();
59
+ };
60
+ }, [instance, consent]);
61
+ if (typeof window === "undefined") {
62
+ return {
63
+ decorateUrl: async (url) => url,
64
+ getVisitId: () => void 0,
65
+ ready: false,
66
+ track: async () => void 0
67
+ };
68
+ }
69
+ return {
70
+ decorateUrl: (url) => instance.decorateUrl(url),
71
+ getVisitId: () => instance.getVisitId(),
72
+ ready: true,
73
+ track: (eventName, props) => instance.track(eventName, props)
74
+ };
75
+ }
76
+
77
+ export { MESH_CDN_URL, MeshAnalytics, useStorefrontAnalytics };
@@ -0,0 +1,189 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const storefrontAnalyticsEvents: {
4
+ readonly cartUpdated: "cart.updated";
5
+ readonly cartUrlCreated: "cart.url_created";
6
+ readonly checkoutStarted: "checkout.started";
7
+ readonly eventView: "event.viewed";
8
+ readonly orderConfirmed: "order.confirmed";
9
+ readonly orderExpired: "order.expired";
10
+ readonly orderRefunded: "order.refunded";
11
+ readonly orderReserved: "order.reserved";
12
+ readonly orderVoided: "order.voided";
13
+ readonly tierView: "tier.viewed";
14
+ readonly visit: "visit.started";
15
+ };
16
+ declare const storefrontAnalyticsEventNameSchema: z.ZodEnum<{
17
+ "cart.updated": "cart.updated";
18
+ "cart.url_created": "cart.url_created";
19
+ "checkout.started": "checkout.started";
20
+ "event.viewed": "event.viewed";
21
+ "order.confirmed": "order.confirmed";
22
+ "order.expired": "order.expired";
23
+ "order.refunded": "order.refunded";
24
+ "order.reserved": "order.reserved";
25
+ "order.voided": "order.voided";
26
+ "tier.viewed": "tier.viewed";
27
+ "visit.started": "visit.started";
28
+ }>;
29
+ type StorefrontAnalyticsEventName = z.infer<typeof storefrontAnalyticsEventNameSchema>;
30
+ declare const analyticsUtmSchema: z.ZodObject<{
31
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
32
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
33
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
34
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
35
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
36
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
37
+ }, z.core.$strip>;
38
+ type AnalyticsUtm = z.infer<typeof analyticsUtmSchema>;
39
+ declare const analyticsConsentStateSchema: z.ZodEnum<{
40
+ granted: "granted";
41
+ denied: "denied";
42
+ unknown: "unknown";
43
+ }>;
44
+ type AnalyticsConsentState = z.infer<typeof analyticsConsentStateSchema>;
45
+ declare const storefrontAttributionSchema: z.ZodObject<{
46
+ cartId: z.ZodOptional<z.ZodString>;
47
+ cartRevision: z.ZodOptional<z.ZodNumber>;
48
+ consentState: z.ZodOptional<z.ZodEnum<{
49
+ granted: "granted";
50
+ denied: "denied";
51
+ unknown: "unknown";
52
+ }>>;
53
+ utm: z.ZodOptional<z.ZodObject<{
54
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
55
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
56
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
57
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
58
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
59
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
60
+ }, z.core.$strip>>;
61
+ visitId: z.ZodString;
62
+ }, z.core.$strip>;
63
+ type StorefrontAttribution = z.infer<typeof storefrontAttributionSchema>;
64
+ declare const serverAnalyticsLineSchema: z.ZodObject<{
65
+ quantity: z.ZodNumber;
66
+ saleKeyId: z.ZodString;
67
+ amountUsdc: z.ZodOptional<z.ZodString>;
68
+ refundId: z.ZodOptional<z.ZodString>;
69
+ }, z.core.$strip>;
70
+ type ServerAnalyticsLine = z.infer<typeof serverAnalyticsLineSchema>;
71
+ declare const browserAnalyticsEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
72
+ cartId: z.ZodOptional<z.ZodString>;
73
+ cartRevision: z.ZodOptional<z.ZodNumber>;
74
+ eventId: z.ZodString;
75
+ referrer: z.ZodOptional<z.ZodString>;
76
+ url: z.ZodOptional<z.ZodString>;
77
+ utm: z.ZodOptional<z.ZodObject<{
78
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
79
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
80
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
81
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
82
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
83
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
84
+ }, z.core.$strip>>;
85
+ visitId: z.ZodOptional<z.ZodString>;
86
+ eventName: z.ZodLiteral<"visit.started">;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ cartId: z.ZodOptional<z.ZodString>;
89
+ cartRevision: z.ZodOptional<z.ZodNumber>;
90
+ eventId: z.ZodString;
91
+ referrer: z.ZodOptional<z.ZodString>;
92
+ url: z.ZodOptional<z.ZodString>;
93
+ utm: z.ZodOptional<z.ZodObject<{
94
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
95
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
96
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
97
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
98
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
99
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
100
+ }, z.core.$strip>>;
101
+ visitId: z.ZodOptional<z.ZodString>;
102
+ eventContractId: z.ZodString;
103
+ eventName: z.ZodLiteral<"event.viewed">;
104
+ }, z.core.$strip>, z.ZodObject<{
105
+ cartId: z.ZodOptional<z.ZodString>;
106
+ cartRevision: z.ZodOptional<z.ZodNumber>;
107
+ eventId: z.ZodString;
108
+ referrer: z.ZodOptional<z.ZodString>;
109
+ url: z.ZodOptional<z.ZodString>;
110
+ utm: z.ZodOptional<z.ZodObject<{
111
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
112
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
113
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
114
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
115
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
116
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
117
+ }, z.core.$strip>>;
118
+ visitId: z.ZodOptional<z.ZodString>;
119
+ eventName: z.ZodLiteral<"tier.viewed">;
120
+ saleKeyId: z.ZodString;
121
+ }, z.core.$strip>, z.ZodObject<{
122
+ eventId: z.ZodString;
123
+ referrer: z.ZodOptional<z.ZodString>;
124
+ url: z.ZodOptional<z.ZodString>;
125
+ utm: z.ZodOptional<z.ZodObject<{
126
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
127
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
128
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
129
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
130
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
131
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
132
+ }, z.core.$strip>>;
133
+ visitId: z.ZodOptional<z.ZodString>;
134
+ cartId: z.ZodString;
135
+ cartRevision: z.ZodNumber;
136
+ eventName: z.ZodLiteral<"cart.updated">;
137
+ lines: z.ZodArray<z.ZodObject<{
138
+ quantity: z.ZodNumber;
139
+ saleKeyId: z.ZodString;
140
+ }, z.core.$strip>>;
141
+ }, z.core.$strip>], "eventName">;
142
+ type BrowserAnalyticsEvent = z.infer<typeof browserAnalyticsEventSchema>;
143
+ declare const trackAnalyticsEventInputSchema: z.ZodObject<{
144
+ cartId: z.ZodOptional<z.ZodString>;
145
+ cartRevision: z.ZodOptional<z.ZodNumber>;
146
+ eventContractId: z.ZodOptional<z.ZodString>;
147
+ eventId: z.ZodString;
148
+ eventName: z.ZodEnum<{
149
+ "cart.updated": "cart.updated";
150
+ "cart.url_created": "cart.url_created";
151
+ "checkout.started": "checkout.started";
152
+ "event.viewed": "event.viewed";
153
+ "order.confirmed": "order.confirmed";
154
+ "order.expired": "order.expired";
155
+ "order.refunded": "order.refunded";
156
+ "order.reserved": "order.reserved";
157
+ "order.voided": "order.voided";
158
+ "tier.viewed": "tier.viewed";
159
+ "visit.started": "visit.started";
160
+ }>;
161
+ lines: z.ZodOptional<z.ZodArray<z.ZodObject<{
162
+ quantity: z.ZodNumber;
163
+ saleKeyId: z.ZodString;
164
+ }, z.core.$strip>>>;
165
+ referrer: z.ZodOptional<z.ZodString>;
166
+ saleKeyId: z.ZodOptional<z.ZodString>;
167
+ sceneId: z.ZodOptional<z.ZodString>;
168
+ url: z.ZodOptional<z.ZodString>;
169
+ utm: z.ZodOptional<z.ZodObject<{
170
+ utm_campaign: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
171
+ utm_content: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
172
+ utm_id: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
173
+ utm_medium: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
174
+ utm_source: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
175
+ utm_term: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
176
+ }, z.core.$strip>>;
177
+ visitId: z.ZodOptional<z.ZodString>;
178
+ }, z.core.$strip>;
179
+ type TrackAnalyticsEventInput = z.infer<typeof trackAnalyticsEventInputSchema>;
180
+ declare const createHandoffTokenInputSchema: z.ZodObject<{
181
+ visitId: z.ZodString;
182
+ }, z.core.$strip>;
183
+ type CreateHandoffTokenInput = z.infer<typeof createHandoffTokenInputSchema>;
184
+ declare const createHandoffTokenOutputSchema: z.ZodObject<{
185
+ token: z.ZodString;
186
+ }, z.core.$strip>;
187
+ type CreateHandoffTokenOutput = z.infer<typeof createHandoffTokenOutputSchema>;
188
+
189
+ export { type AnalyticsConsentState, type AnalyticsUtm, type BrowserAnalyticsEvent, type CreateHandoffTokenInput, type CreateHandoffTokenOutput, type ServerAnalyticsLine, type StorefrontAnalyticsEventName, type StorefrontAttribution, type TrackAnalyticsEventInput, analyticsConsentStateSchema, analyticsUtmSchema, browserAnalyticsEventSchema, createHandoffTokenInputSchema, createHandoffTokenOutputSchema, serverAnalyticsLineSchema, storefrontAnalyticsEventNameSchema, storefrontAnalyticsEvents, storefrontAttributionSchema, trackAnalyticsEventInputSchema };
@@ -0,0 +1 @@
1
+ export { analyticsConsentStateSchema, analyticsUtmSchema, browserAnalyticsEventSchema, createHandoffTokenInputSchema, createHandoffTokenOutputSchema, serverAnalyticsLineSchema, storefrontAnalyticsEventNameSchema, storefrontAnalyticsEvents, storefrontAttributionSchema, trackAnalyticsEventInputSchema } from './chunk-NTTEMMBO.js';
@@ -0,0 +1,46 @@
1
+ /** Mint a visit id server-side (e.g. for the first backend-seen shopper). */
2
+ declare function createVisitId(): `${string}-${string}-${string}-${string}-${string}`;
3
+ /**
4
+ * Read attribution from an incoming request (first-party `mesh_visit`
5
+ * cookie). Returns the visit for the Storefront API passthrough; the
6
+ * backend merges its own cart state around it.
7
+ */
8
+ declare function getAttribution(req: {
9
+ headers: {
10
+ get: (name: string) => string | null;
11
+ };
12
+ }): {
13
+ visitId: string | undefined;
14
+ };
15
+ type TrackServerEventInput = {
16
+ amountUsdc?: string;
17
+ cartId?: string;
18
+ cartRevision?: number;
19
+ eventContractId?: string;
20
+ eventId?: string;
21
+ eventName: string;
22
+ lines?: Array<{
23
+ quantity: number;
24
+ saleKeyId: string;
25
+ }>;
26
+ orderId?: string;
27
+ saleKeyId?: string;
28
+ url?: string;
29
+ utm?: Record<string, string | undefined>;
30
+ visitId?: string;
31
+ };
32
+ /**
33
+ * Emit a server-side attribution event (webhook conversions, backend
34
+ * cart updates). Uses the secret `sk_` route — server-only.
35
+ */
36
+ declare function trackServerEvent(input: {
37
+ apiBase: string;
38
+ apiKey: string;
39
+ event: TrackServerEventInput;
40
+ fetcher?: typeof fetch;
41
+ }): Promise<{
42
+ accepted: boolean;
43
+ visitId: string;
44
+ }>;
45
+
46
+ export { type TrackServerEventInput, createVisitId, getAttribution, trackServerEvent };
package/dist/server.js ADDED
@@ -0,0 +1,46 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ // src/server.ts
4
+ function createVisitId() {
5
+ return randomUUID();
6
+ }
7
+ function parseCookies(header) {
8
+ const cookies = /* @__PURE__ */ new Map();
9
+ if (!header) return cookies;
10
+ for (const part of header.split(";")) {
11
+ const index = part.indexOf("=");
12
+ if (index <= 0) continue;
13
+ const name = part.slice(0, index).trim();
14
+ const value = part.slice(index + 1).trim();
15
+ if (name) cookies.set(name, decodeURIComponent(value));
16
+ }
17
+ return cookies;
18
+ }
19
+ function getAttribution(req) {
20
+ const visitId = parseCookies(req.headers.get("cookie")).get("mesh_visit");
21
+ return { visitId };
22
+ }
23
+ async function trackServerEvent(input) {
24
+ const response = await (input.fetcher ?? fetch)(
25
+ `${input.apiBase.replace(/\/+$/, "")}/v1/analytics/track/server`,
26
+ {
27
+ body: JSON.stringify({
28
+ eventId: input.event.eventId ?? randomUUID(),
29
+ ...input.event
30
+ }),
31
+ headers: {
32
+ Authorization: `Bearer ${input.apiKey}`,
33
+ "Content-Type": "application/json"
34
+ },
35
+ method: "POST"
36
+ }
37
+ );
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `Storefront analytics server track failed with status ${response.status}.`
41
+ );
42
+ }
43
+ return await response.json();
44
+ }
45
+
46
+ export { createVisitId, getAttribution, trackServerEvent };
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "author": "Gatekeeper Team",
3
+ "description": "Browser/server analytics helpers and shared schemas for Mesh storefront attribution",
4
+ "devDependencies": {
5
+ "@types/node": "^22.19.3",
6
+ "@types/react": "19.2.7",
7
+ "@types/react-dom": "19.2.3",
8
+ "next": "15.5.25",
9
+ "react": "19.2.3",
10
+ "react-dom": "19.2.3",
11
+ "tsup": "^8.5.1",
12
+ "typescript": "^5.9.3",
13
+ "vitest": "^3.2.6",
14
+ "zod": "4.0.17"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./client": {
22
+ "import": "./dist/client.js",
23
+ "types": "./dist/client.d.ts"
24
+ },
25
+ "./react": {
26
+ "import": "./dist/react.js",
27
+ "types": "./dist/react.d.ts"
28
+ },
29
+ "./schemas": {
30
+ "import": "./dist/schemas.js",
31
+ "types": "./dist/schemas.d.ts"
32
+ },
33
+ "./server": {
34
+ "import": "./dist/server.js",
35
+ "types": "./dist/server.d.ts"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist/**/*.d.ts",
40
+ "dist/**/*.js",
41
+ "README.md"
42
+ ],
43
+ "keywords": [
44
+ "mesh",
45
+ "storefront",
46
+ "analytics",
47
+ "typescript"
48
+ ],
49
+ "license": "AGPL-3.0",
50
+ "main": "./dist/index.js",
51
+ "name": "@sceneinfrastructure/storefront-analytics",
52
+ "peerDependencies": {
53
+ "next": "15.5.25",
54
+ "react": "~19.2.3",
55
+ "zod": "4.0.17"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "next": {
59
+ "optional": true
60
+ },
61
+ "react": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "private": false,
66
+ "publishConfig": {
67
+ "@sceneinfrastructure:registry": "https://registry.npmjs.org",
68
+ "access": "public",
69
+ "registry": "https://registry.npmjs.org"
70
+ },
71
+ "repository": {
72
+ "directory": "packages/storefront-analytics",
73
+ "type": "git",
74
+ "url": "https://github.com/sceneinfrastructure/gatekeeper"
75
+ },
76
+ "sideEffects": false,
77
+ "type": "module",
78
+ "types": "./dist/index.d.ts",
79
+ "typesVersions": {
80
+ "*": {
81
+ "*": [
82
+ "dist/index.d.ts"
83
+ ],
84
+ "client": [
85
+ "dist/client.d.ts"
86
+ ],
87
+ "react": [
88
+ "dist/react.d.ts"
89
+ ],
90
+ "schemas": [
91
+ "dist/schemas.d.ts"
92
+ ],
93
+ "server": [
94
+ "dist/server.d.ts"
95
+ ]
96
+ }
97
+ },
98
+ "typings": "./dist/index.d.ts",
99
+ "version": "0.2.0",
100
+ "scripts": {
101
+ "build": "tsup",
102
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
103
+ "dev": "tsup --watch",
104
+ "test": "vitest run",
105
+ "turbo:dev": "tsup --watch",
106
+ "turbo:setup:dev": "wait-on dist/index.js",
107
+ "typecheck": "tsc --noEmit"
108
+ }
109
+ }