@genlook/storefront 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/README.md +180 -0
- package/dist/analytics.d.ts +26 -0
- package/dist/analytics.js +7 -0
- package/dist/anonymous-id.d.ts +30 -0
- package/dist/anonymous-id.js +33 -0
- package/dist/client.d.ts +289 -0
- package/dist/client.js +465 -0
- package/dist/consent.d.ts +17 -0
- package/dist/consent.js +34 -0
- package/dist/create-client.d.ts +84 -0
- package/dist/create-client.js +131 -0
- package/dist/default-tracking.d.ts +64 -0
- package/dist/default-tracking.js +101 -0
- package/dist/email.d.ts +5 -0
- package/dist/email.js +24 -0
- package/dist/entities.d.ts +186 -0
- package/dist/entities.js +47 -0
- package/dist/erasure.d.ts +36 -0
- package/dist/erasure.js +23 -0
- package/dist/events.d.ts +226 -0
- package/dist/events.js +41 -0
- package/dist/fetch-transport.d.ts +65 -0
- package/dist/fetch-transport.js +112 -0
- package/dist/generation.d.ts +132 -0
- package/dist/generation.js +382 -0
- package/dist/history.d.ts +76 -0
- package/dist/history.js +68 -0
- package/dist/memory-storage.d.ts +10 -0
- package/dist/memory-storage.js +12 -0
- package/dist/pending-upload.d.ts +32 -0
- package/dist/pending-upload.js +13 -0
- package/dist/persistence.d.ts +12 -0
- package/dist/persistence.js +101 -0
- package/dist/policy.d.ts +24 -0
- package/dist/policy.js +45 -0
- package/dist/ports.d.ts +187 -0
- package/dist/ports.js +1 -0
- package/dist/public-api.d.ts +235 -0
- package/dist/public-api.js +1 -0
- package/dist/public.d.ts +33 -0
- package/dist/public.js +12 -0
- package/dist/settings.d.ts +38 -0
- package/dist/settings.js +70 -0
- package/dist/sharing.d.ts +26 -0
- package/dist/sharing.js +42 -0
- package/dist/storage-adapters.d.ts +69 -0
- package/dist/storage-adapters.js +82 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +23 -0
- package/dist/tracker.d.ts +124 -0
- package/dist/tracker.js +229 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/upload.d.ts +123 -0
- package/dist/upload.js +176 -0
- package/dist/usage.d.ts +45 -0
- package/dist/usage.js +110 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merchant-owned widget settings, as `GET /settings` returns them.
|
|
3
|
+
*
|
|
4
|
+
* The storefront widget reads these off its inline theme config; an SDK
|
|
5
|
+
* integrator has no such config, so the factory pulls them from the store. Pure
|
|
6
|
+
* functions over the KVStorage + Transport ports (type-only imports), so the
|
|
7
|
+
* module stays value-self-contained and directly testable.
|
|
8
|
+
*/
|
|
9
|
+
import type { KVStorage, Transport } from "./ports";
|
|
10
|
+
export interface StoreSettings {
|
|
11
|
+
maxGenerations: number;
|
|
12
|
+
period: "daily" | "weekly";
|
|
13
|
+
emailCollectionStep: number;
|
|
14
|
+
loggedInCustomersOnly: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Cache window. Fresher than this and the factory issues no request at all. */
|
|
17
|
+
export declare const SETTINGS_CACHE_TTL_MS: number;
|
|
18
|
+
export declare const SETTINGS_ENDPOINT = "/settings";
|
|
19
|
+
/**
|
|
20
|
+
* Read a settings payload field by field. Per-field rather than all-or-nothing:
|
|
21
|
+
* the cascade is itself per-field, so a response missing a key means "no server
|
|
22
|
+
* opinion here", not "discard the whole thing". Returns null when nothing usable
|
|
23
|
+
* was found.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseSettings(raw: unknown): Partial<StoreSettings> | null;
|
|
26
|
+
/**
|
|
27
|
+
* Cached settings for this store, with whether they are still inside the TTL.
|
|
28
|
+
* A stale entry is still returned — it seeds the state immediately while the
|
|
29
|
+
* background refresh runs, so an offline app keeps the last known settings.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadSettings(storage: KVStorage, storeId: string, now: number): {
|
|
32
|
+
settings: Partial<StoreSettings>;
|
|
33
|
+
fresh: boolean;
|
|
34
|
+
} | null;
|
|
35
|
+
export declare function saveSettings(storage: KVStorage, storeId: string, settings: Partial<StoreSettings>, now: number): void;
|
|
36
|
+
/** `GET /settings`. Never rejects — a failure returns null and the cascade keeps
|
|
37
|
+
* whatever it already resolved. */
|
|
38
|
+
export declare function fetchSettings(transport: Transport): Promise<Partial<StoreSettings> | null>;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const SETTINGS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
2
|
+
export const SETTINGS_ENDPOINT = "/settings";
|
|
3
|
+
function storageKey(storeId) {
|
|
4
|
+
return `tryon-settings:${storeId || "default"}`;
|
|
5
|
+
}
|
|
6
|
+
export function parseSettings(raw) {
|
|
7
|
+
if (!raw || typeof raw !== "object")
|
|
8
|
+
return null;
|
|
9
|
+
const r = raw;
|
|
10
|
+
const out = {};
|
|
11
|
+
if (typeof r.maxGenerations === "number" && Number.isFinite(r.maxGenerations)) {
|
|
12
|
+
out.maxGenerations = r.maxGenerations;
|
|
13
|
+
}
|
|
14
|
+
if (typeof r.emailCollectionStep === "number" && Number.isFinite(r.emailCollectionStep)) {
|
|
15
|
+
out.emailCollectionStep = r.emailCollectionStep;
|
|
16
|
+
}
|
|
17
|
+
if (r.period === "daily" || r.period === "weekly")
|
|
18
|
+
out.period = r.period;
|
|
19
|
+
if (typeof r.loggedInCustomersOnly === "boolean") {
|
|
20
|
+
out.loggedInCustomersOnly = r.loggedInCustomersOnly;
|
|
21
|
+
}
|
|
22
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
23
|
+
}
|
|
24
|
+
export function loadSettings(storage, storeId, now) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = storage.getItem(storageKey(storeId));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!raw)
|
|
33
|
+
return null;
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || typeof parsed !== "object")
|
|
42
|
+
return null;
|
|
43
|
+
const record = parsed;
|
|
44
|
+
if (typeof record.ts !== "number" || !Number.isFinite(record.ts))
|
|
45
|
+
return null;
|
|
46
|
+
const settings = parseSettings(record.settings);
|
|
47
|
+
if (!settings)
|
|
48
|
+
return null;
|
|
49
|
+
const age = now - record.ts;
|
|
50
|
+
return { settings, fresh: age >= 0 && age < SETTINGS_CACHE_TTL_MS };
|
|
51
|
+
}
|
|
52
|
+
export function saveSettings(storage, storeId, settings, now) {
|
|
53
|
+
try {
|
|
54
|
+
const record = { settings, ts: now };
|
|
55
|
+
storage.setItem(storageKey(storeId), JSON.stringify(record));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function fetchSettings(transport) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await transport.fetch(SETTINGS_ENDPOINT, { method: "GET" });
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return null;
|
|
65
|
+
return parseSettings(await response.json());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Transport } from "./ports";
|
|
2
|
+
import type { ShareGenerationResponse } from "./types";
|
|
3
|
+
import type { Store } from "./store";
|
|
4
|
+
import type { CoreState } from "./entities";
|
|
5
|
+
export declare function createShareLink(transport: Transport, generationId: string, effectiveDomain?: string, productUrl?: string): Promise<ShareGenerationResponse>;
|
|
6
|
+
export interface ShareEngineDeps {
|
|
7
|
+
transport: Transport;
|
|
8
|
+
store: Store<CoreState>;
|
|
9
|
+
/** Emits tryon:share_link_created (bound to analytics by the client). Absent on
|
|
10
|
+
* the short-circuit path — a cached hit creates no link, so nothing fires. */
|
|
11
|
+
emitShareLinkCreated?: (input: {
|
|
12
|
+
generationId: string;
|
|
13
|
+
entryId: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface GetShareUrlOptions {
|
|
17
|
+
effectiveDomain?: string;
|
|
18
|
+
productUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the share URL for a history entry: short-circuit on a cached/prebaked
|
|
22
|
+
* `entry.shareUrl` (zero network — scripted demo entries rely on this), else
|
|
23
|
+
* create a share link for its generation and write the URL back into the entry
|
|
24
|
+
* (write-through cache, so the next share is free).
|
|
25
|
+
*/
|
|
26
|
+
export declare function getShareUrl(deps: ShareEngineDeps, entryId: string, opts?: GetShareUrlOptions): Promise<string>;
|
package/dist/sharing.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export async function createShareLink(transport, generationId, effectiveDomain, productUrl) {
|
|
2
|
+
try {
|
|
3
|
+
const endpoint = `/try-ons/${encodeURIComponent(generationId)}/share`;
|
|
4
|
+
console.log("Creating share link at:", endpoint);
|
|
5
|
+
const response = await transport.fetch(endpoint, {
|
|
6
|
+
method: "POST",
|
|
7
|
+
headers: { "Content-Type": "application/json" },
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
effectiveDomain: effectiveDomain || undefined,
|
|
10
|
+
productUrl: productUrl || undefined,
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const errorText = await response.text();
|
|
15
|
+
throw new Error(`Failed to create share link: ${response.status} ${response.statusText}. ${errorText}`);
|
|
16
|
+
}
|
|
17
|
+
const result = (await response.json());
|
|
18
|
+
console.log("Share link created:", result.shareUrl);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof Error)
|
|
23
|
+
throw error;
|
|
24
|
+
throw new Error(`Unexpected error: ${error}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function getShareUrl(deps, entryId, opts = {}) {
|
|
28
|
+
const entry = deps.store.getState().history.find((r) => r.id === entryId);
|
|
29
|
+
if (!entry)
|
|
30
|
+
throw new Error(`Unknown history entry: ${entryId}`);
|
|
31
|
+
if (entry.shareUrl)
|
|
32
|
+
return entry.shareUrl;
|
|
33
|
+
if (!entry.generationId)
|
|
34
|
+
throw new Error("Generation ID is required to create share link");
|
|
35
|
+
const response = await createShareLink(deps.transport, entry.generationId, opts.effectiveDomain, opts.productUrl);
|
|
36
|
+
deps.store.setState((s) => ({
|
|
37
|
+
...s,
|
|
38
|
+
history: s.history.map((r) => r.id === entryId ? { ...r, shareUrl: response.shareUrl } : r),
|
|
39
|
+
}));
|
|
40
|
+
deps.emitShareLinkCreated?.({ generationId: entry.generationId, entryId });
|
|
41
|
+
return response.shareUrl;
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete {@link KVStorage} adapters shipped with the core, so an integrator
|
|
3
|
+
* holding a publishable key does not have to write one.
|
|
4
|
+
*
|
|
5
|
+
* {@link KVStorage} is deliberately SYNCHRONOUS (`getItem` returns
|
|
6
|
+
* `string | null`, not a promise): the core reads usage, jobs, history and
|
|
7
|
+
* consent during construction, and making the port async would turn every
|
|
8
|
+
* domain call into a promise chain. Hosts whose only store is async (React
|
|
9
|
+
* Native AsyncStorage) get {@link createHydratedStorage} instead.
|
|
10
|
+
*/
|
|
11
|
+
import type { KVStorage } from "./ports";
|
|
12
|
+
/** The `localStorage`/`sessionStorage` shape. Declared locally because the core
|
|
13
|
+
* tsconfig keeps "dom" out of `lib` (the headlessness guard). */
|
|
14
|
+
export interface WebStorageLike {
|
|
15
|
+
getItem(key: string): string | null;
|
|
16
|
+
setItem(key: string, value: string): void;
|
|
17
|
+
removeItem(key: string): void;
|
|
18
|
+
}
|
|
19
|
+
/** True when a usable `localStorage` is present (absent in RN, and access can
|
|
20
|
+
* throw outright in a partitioned/blocked-cookies browser context). */
|
|
21
|
+
export declare function isLocalStorageAvailable(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Browser adapter over `localStorage` (or any storage passed in — a namespaced
|
|
24
|
+
* wrapper, `sessionStorage`, a test double). Throws when no storage is
|
|
25
|
+
* reachable, so a misconfigured host fails loudly instead of silently losing
|
|
26
|
+
* every shopper's history on reload.
|
|
27
|
+
*/
|
|
28
|
+
export declare function localStorageAdapter(store?: WebStorageLike): KVStorage;
|
|
29
|
+
/**
|
|
30
|
+
* Minimal async key/value store — the shape of React Native's AsyncStorage, so
|
|
31
|
+
* the core can be handed one without depending on any mobile package. Either
|
|
32
|
+
* `multiGet` or `getItem` is enough for hydration; `multiGet` is preferred (one
|
|
33
|
+
* bridge round-trip instead of N).
|
|
34
|
+
*/
|
|
35
|
+
export interface AsyncKVStore {
|
|
36
|
+
getAllKeys(): Promise<readonly string[]>;
|
|
37
|
+
multiGet?(keys: readonly string[]): Promise<readonly (readonly [string, string | null])[]>;
|
|
38
|
+
getItem?(key: string): Promise<string | null>;
|
|
39
|
+
setItem(key: string, value: string): Promise<void>;
|
|
40
|
+
removeItem(key: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
export interface HydratedStorageOptions {
|
|
43
|
+
/** Hydrate only keys starting with this prefix (skips the host app's own
|
|
44
|
+
* keys). Omit to preload everything the store holds. */
|
|
45
|
+
keyPrefix?: string;
|
|
46
|
+
/** Called when a write-through fails. Default: `console.warn`. Never fatal —
|
|
47
|
+
* the in-memory value already took effect. */
|
|
48
|
+
onWriteError?: (error: unknown, key: string) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Async store → synchronous {@link KVStorage}, by preloading it into memory.
|
|
52
|
+
*
|
|
53
|
+
* Reads are served from the in-memory snapshot; writes go to memory
|
|
54
|
+
* synchronously AND are mirrored to the async store (write-through). A failed
|
|
55
|
+
* mirror is reported and swallowed: the session keeps working, only durability
|
|
56
|
+
* is lost for that key.
|
|
57
|
+
*
|
|
58
|
+
* IMPORTANT: the returned storage only reflects persisted state once this
|
|
59
|
+
* promise resolves. Await it before constructing the client — a client built on
|
|
60
|
+
* a not-yet-hydrated store starts from empty state and would mint a second
|
|
61
|
+
* anonymous id.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createHydratedStorage(store: AsyncKVStore, options?: HydratedStorageOptions): Promise<KVStorage>;
|
|
64
|
+
/**
|
|
65
|
+
* Best-effort default storage: `localStorage` when the runtime has one,
|
|
66
|
+
* otherwise an in-memory store (state lasts the session only). Used by
|
|
67
|
+
* {@link createTryOnClient} when no `storage` is supplied.
|
|
68
|
+
*/
|
|
69
|
+
export declare function defaultStorage(): KVStorage;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { memoryStorage } from "./memory-storage";
|
|
2
|
+
function globalLocalStorage() {
|
|
3
|
+
return globalThis.localStorage;
|
|
4
|
+
}
|
|
5
|
+
export function isLocalStorageAvailable() {
|
|
6
|
+
try {
|
|
7
|
+
const store = globalLocalStorage();
|
|
8
|
+
if (!store)
|
|
9
|
+
return false;
|
|
10
|
+
const probe = "__genlook_probe__";
|
|
11
|
+
store.setItem(probe, "1");
|
|
12
|
+
store.removeItem(probe);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function localStorageAdapter(store) {
|
|
20
|
+
const target = store ?? globalLocalStorage();
|
|
21
|
+
if (!target) {
|
|
22
|
+
throw new Error("[Genlook] localStorage is not available in this runtime. Pass a `storage` adapter (see createHydratedStorage for React Native).");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
getItem: (key) => target.getItem(key),
|
|
26
|
+
setItem: (key, value) => target.setItem(key, String(value)),
|
|
27
|
+
removeItem: (key) => target.removeItem(key),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function createHydratedStorage(store, options = {}) {
|
|
31
|
+
const memory = memoryStorage();
|
|
32
|
+
const onWriteError = options.onWriteError ??
|
|
33
|
+
((error, key) => {
|
|
34
|
+
console.warn("[Genlook] Storage write failed:", key, error);
|
|
35
|
+
});
|
|
36
|
+
const allKeys = await store.getAllKeys();
|
|
37
|
+
const keys = options.keyPrefix
|
|
38
|
+
? allKeys.filter((key) => key.startsWith(options.keyPrefix))
|
|
39
|
+
: allKeys;
|
|
40
|
+
if (keys.length > 0) {
|
|
41
|
+
if (typeof store.multiGet === "function") {
|
|
42
|
+
for (const [key, value] of await store.multiGet(keys)) {
|
|
43
|
+
if (value != null)
|
|
44
|
+
memory.setItem(key, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (typeof store.getItem === "function") {
|
|
48
|
+
const getItem = store.getItem.bind(store);
|
|
49
|
+
const values = await Promise.all(keys.map((key) => getItem(key)));
|
|
50
|
+
keys.forEach((key, i) => {
|
|
51
|
+
const value = values[i];
|
|
52
|
+
if (value != null)
|
|
53
|
+
memory.setItem(key, value);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error("[Genlook] AsyncKVStore must implement multiGet or getItem to hydrate.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
getItem: (key) => memory.getItem(key),
|
|
62
|
+
setItem: (key, value) => {
|
|
63
|
+
const str = String(value);
|
|
64
|
+
memory.setItem(key, str);
|
|
65
|
+
void Promise.resolve()
|
|
66
|
+
.then(() => store.setItem(key, str))
|
|
67
|
+
.catch((error) => onWriteError(error, key));
|
|
68
|
+
},
|
|
69
|
+
removeItem: (key) => {
|
|
70
|
+
memory.removeItem(key);
|
|
71
|
+
void Promise.resolve()
|
|
72
|
+
.then(() => store.removeItem(key))
|
|
73
|
+
.catch((error) => onWriteError(error, key));
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function defaultStorage() {
|
|
78
|
+
if (isLocalStorageAvailable())
|
|
79
|
+
return localStorageAdapter();
|
|
80
|
+
console.warn("[Genlook] No localStorage available — falling back to in-memory storage. Try-on history and quota will not survive a reload.");
|
|
81
|
+
return memoryStorage();
|
|
82
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Listener = () => void;
|
|
2
|
+
/** Minimal observable store. Zero dependencies. */
|
|
3
|
+
export interface Store<S> {
|
|
4
|
+
getState(): S;
|
|
5
|
+
subscribe(listener: Listener): () => void;
|
|
6
|
+
/** Replace state with the updater's result; notifies listeners only on identity change. */
|
|
7
|
+
setState(updater: (prev: S) => S): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createStore<S>(initial: S): Store<S>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createStore(initial) {
|
|
2
|
+
let state = initial;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
return {
|
|
5
|
+
getState() {
|
|
6
|
+
return state;
|
|
7
|
+
},
|
|
8
|
+
subscribe(listener) {
|
|
9
|
+
listeners.add(listener);
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(listener);
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
setState(updater) {
|
|
15
|
+
const next = updater(state);
|
|
16
|
+
if (next === state)
|
|
17
|
+
return;
|
|
18
|
+
state = next;
|
|
19
|
+
for (const listener of [...listeners])
|
|
20
|
+
listener();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Transport, ConsentSource, PageContext } from "./ports";
|
|
2
|
+
/** Periodic-flush cadence (ms). The batch queue is core-owned; the DOM timer that
|
|
3
|
+
* ticks at this interval is registered by the host runtime. */
|
|
4
|
+
export declare const FLUSH_INTERVAL_MS = 5000;
|
|
5
|
+
/** Minimal failed-response shape captureFetchError reads. The browser `Response`
|
|
6
|
+
* (passed by the HttpClient) structurally satisfies it — the core needs no dom. */
|
|
7
|
+
interface FetchErrorResponse {
|
|
8
|
+
status: number;
|
|
9
|
+
text(): Promise<string>;
|
|
10
|
+
headers: {
|
|
11
|
+
forEach(cb: (value: string, key: string) => void): void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface TrackerDeps {
|
|
15
|
+
transport: Transport;
|
|
16
|
+
consent: ConsentSource;
|
|
17
|
+
pageContext: PageContext;
|
|
18
|
+
/** Secure uuid factory (host-provided; the core takes no crypto dependency). */
|
|
19
|
+
uuid: () => string;
|
|
20
|
+
/** Injectable clock (tests). Defaults to Date.now. */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The single tracking pipeline. `capture`/`captureFetchError` and the property
|
|
25
|
+
* name `tracking` are the frozen back-compat surface consumed by widget UI code,
|
|
26
|
+
* so both keep their exact signatures.
|
|
27
|
+
*/
|
|
28
|
+
export declare class Tracker {
|
|
29
|
+
private queue;
|
|
30
|
+
private pageviewId;
|
|
31
|
+
private lastPathname;
|
|
32
|
+
private isFlushing;
|
|
33
|
+
private readonly isBot;
|
|
34
|
+
private consentSnapshot;
|
|
35
|
+
private consentSubscribed;
|
|
36
|
+
private uiVariant;
|
|
37
|
+
private uiBuild;
|
|
38
|
+
/** Widget-scope consent override (see grantWidgetScopeConsent). */
|
|
39
|
+
private widgetScopeConsent;
|
|
40
|
+
/** In-memory identity fallbacks, minted on first need if the host can't supply
|
|
41
|
+
* persisted ids. Ensures a batch is NEVER built without an anonymous_id +
|
|
42
|
+
* session_id (the backend now requires both). Prefixed to match the ^anon_/
|
|
43
|
+
* ^sess_ wire contract; stable for the tracker's lifetime once minted. */
|
|
44
|
+
private fallbackAnonymousId;
|
|
45
|
+
private fallbackSessionId;
|
|
46
|
+
private readonly transport;
|
|
47
|
+
private readonly consent;
|
|
48
|
+
private readonly pageContext;
|
|
49
|
+
private readonly uuid;
|
|
50
|
+
private readonly now;
|
|
51
|
+
private static readonly MAX_QUEUE_SIZE;
|
|
52
|
+
private static readonly EARLY_FLUSH_SIZE;
|
|
53
|
+
constructor(deps: TrackerDeps);
|
|
54
|
+
/** Stamp the active experiment variant onto every subsequent batch context
|
|
55
|
+
* (`ui_variant`). Additive, BQ-safe; pass null to clear. */
|
|
56
|
+
setUiVariant(variant: string | null): void;
|
|
57
|
+
/** Stamp `ui_version` onto every subsequent batch context. Called by the UI
|
|
58
|
+
* bundle once it loads, reporting its OWN content hash — the boot's
|
|
59
|
+
* `app_version` cannot, because the UI bundle is lazy-loaded and hashed
|
|
60
|
+
* separately, so a shopper can run a new boot against a cached older UI. */
|
|
61
|
+
setUiBuild(build: string | null): void;
|
|
62
|
+
/** Event-category prefixes that a widget-scope consent grant unlocks while SITE
|
|
63
|
+
* analytics consent is denied. product_page:* is deliberately absent — it
|
|
64
|
+
* stays dropped until site consent is (re)granted. */
|
|
65
|
+
private static readonly WIDGET_SCOPE_PREFIXES;
|
|
66
|
+
/** SITE-LEVEL analytics gate. Public so the experiment layer can tell whether
|
|
67
|
+
* an exposure would be silently dropped before emitting it. Only an explicit
|
|
68
|
+
* analytics=false snapshot blocks; true / null / undefined / no consent source
|
|
69
|
+
* all allow (the Shopify Customer Privacy snapshot encodes regional rules). */
|
|
70
|
+
isTrackingAllowed(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Grant the widget-scope consent override. After the shopper accepts the
|
|
73
|
+
* widget's own legal-consent gate, widget:/tryon:/sheet:/api: events ARE
|
|
74
|
+
* captured and sent (with full identity) even while SITE analytics consent is
|
|
75
|
+
* denied; product_page:* events stay dropped. Idempotent. The grant is
|
|
76
|
+
* re-derived on reload from the persisted legal-consent record (TryOnClient),
|
|
77
|
+
* so it survives a page refresh.
|
|
78
|
+
*/
|
|
79
|
+
grantWidgetScopeConsent(): void;
|
|
80
|
+
/** Per-event gate: site consent allows everything; otherwise only widget-scope
|
|
81
|
+
* events pass, and only once the widget-scope grant is set. */
|
|
82
|
+
private isEventAllowed;
|
|
83
|
+
capture(event: string, properties?: Record<string, unknown>): void;
|
|
84
|
+
captureFetchError(url: string, method: string, error: Error | FetchErrorResponse): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Drain the queue to /events. `beacon` requests unload semantics
|
|
87
|
+
* (sendBeacon / keepalive) from the transport. One path for both the periodic
|
|
88
|
+
* flush and the page-hide flush — the wire body is identical either way.
|
|
89
|
+
*/
|
|
90
|
+
flush(opts?: {
|
|
91
|
+
beacon?: boolean;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
private enqueue;
|
|
94
|
+
/**
|
|
95
|
+
* Read the consent snapshot once and subscribe to changes. Cheap: the snapshot
|
|
96
|
+
* is cached and refreshed via the change hook, so batches never trigger a
|
|
97
|
+
* per-event CMP call. Idempotent — safe to call every capture/flush.
|
|
98
|
+
*/
|
|
99
|
+
private ensureConsentSubscribed;
|
|
100
|
+
/**
|
|
101
|
+
* On explicit consent deny (`analytics === "no"`), drop the two tracking-owned
|
|
102
|
+
* storage keys and the cached session id via the PageContext port. Functional
|
|
103
|
+
* `genlook-*` storage (quota counters, photo history) sits on a different legal
|
|
104
|
+
* basis and is deliberately left untouched (the port only removes those keys).
|
|
105
|
+
*/
|
|
106
|
+
private handleConsentChange;
|
|
107
|
+
/**
|
|
108
|
+
* Assemble the batch context. Every sent batch is fully identified (the gate
|
|
109
|
+
* already dropped denied captures), so identity + attribution + referrer always
|
|
110
|
+
* ride. The pageview id is a plain in-memory value, re-minted when the
|
|
111
|
+
* pathname changes.
|
|
112
|
+
*/
|
|
113
|
+
private buildContext;
|
|
114
|
+
/**
|
|
115
|
+
* Read the host identity context, guaranteeing a non-empty anonymous_id +
|
|
116
|
+
* session_id. Normally the host supplies persisted ids; if that read throws
|
|
117
|
+
* (storage unavailable) or returns empty ids, in-memory fallbacks fill in so a
|
|
118
|
+
* fully-identified batch is still built — a batch is never sent without ids.
|
|
119
|
+
*/
|
|
120
|
+
private readIdentityContext;
|
|
121
|
+
private getFallbackAnonymousId;
|
|
122
|
+
private getFallbackSessionId;
|
|
123
|
+
}
|
|
124
|
+
export {};
|