@studio-vaai/js-tracking 0.1.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) studio vaai GmbH
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,71 @@
1
+ # @studio-vaai/js-tracking
2
+
3
+ Typed analytics events for studio vaai, plus a beacon-based [Umami](https://umami.is)
4
+ transport.
5
+
6
+ ## Public surface
7
+
8
+ ```ts
9
+ interface TrackingClient {
10
+ trackEvent(event: TrackingEvent, options?: TrackOptions): TrackResult;
11
+ }
12
+ ```
13
+
14
+ Synchronous and fire-and-forget — the default transport dispatches via
15
+ `navigator.sendBeacon` so events survive page unload and navigation. The
16
+ returned `TrackResult` reflects whether the browser accepted the beacon (or
17
+ the event was queued for later flush), not whether the server acknowledged it.
18
+
19
+ Event-specific by design: every event must appear in `TrackingEvent` (see
20
+ [`src/events.ts`](src/events.ts)). Until the first variant lands, `trackEvent`
21
+ is uncallable — which forces a central declaration of every event the SDK can
22
+ send.
23
+
24
+ The Umami-script-style `track()` / `identify()` overloads live as
25
+ closure-private helpers inside the transport in
26
+ [`src/impl/fetch-umami-tracking-client.ts`](src/impl/fetch-umami-tracking-client.ts);
27
+ they are not part of the public API.
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { createFetchUmamiTrackingClient } from "@studio-vaai/js-tracking";
33
+
34
+ // Static config: websiteId known at construction.
35
+ const tracking = createFetchUmamiTrackingClient({
36
+ sdkVersion: "@studio-vaai/react-components@0.1.3",
37
+ websiteId: "00000000-0000-0000-0000-000000000000",
38
+ // baseUrl defaults to https://a.studio-vaai.com
39
+ });
40
+
41
+ // Once you've declared the event in src/events.ts:
42
+ tracking.trackEvent({ name: "try_on_started", data: { productId } });
43
+ ```
44
+
45
+ ### Lazy resolver (per-shop config)
46
+
47
+ Pass `resolveConfig` instead of `websiteId` when the Umami website is only
48
+ known after an async lookup (e.g. fetched via `client.tracking.getTracking()`).
49
+ Events fired before the resolver settles are buffered internally and flushed
50
+ in order on success.
51
+
52
+ ```ts
53
+ const tracking = createFetchUmamiTrackingClient({
54
+ sdkVersion: "@studio-vaai/react-components@0.1.3",
55
+ resolveConfig: async () => {
56
+ const r = await client.tracking.getTracking({});
57
+ if (!r.ok) throw new ApiFailureError(r.error);
58
+ return { websiteId: r.data.umamiWebsiteId, baseUrl: r.data.umamiBaseUrl };
59
+ },
60
+ });
61
+ ```
62
+
63
+ ## Adding an event
64
+
65
+ Declare it in [`src/events.ts`](src/events.ts) as a new `TrackingEvent` union
66
+ variant (snake_case `name`, flat-primitive `data`). Call sites then get full
67
+ autocomplete + type-checking on `name` and `data`.
68
+
69
+ ## License
70
+
71
+ MIT
package/dist/base.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ export type TrackErrorKind = "network" | "rejected" | "unknown";
2
+ export interface TrackError {
3
+ kind: TrackErrorKind;
4
+ message: string;
5
+ details?: unknown;
6
+ }
7
+ export interface TrackSuccess {
8
+ ok: true;
9
+ }
10
+ export interface TrackFailure {
11
+ ok: false;
12
+ error: TrackError;
13
+ }
14
+ /**
15
+ * Fire-and-mostly-forget result. Tracking is best-effort: callers should not
16
+ * branch product behaviour on `ok`, but may surface a warning in dev.
17
+ *
18
+ * Returned synchronously from {@link TrackingClient.trackEvent} — reflects
19
+ * whether the event was queued for delivery (via the Beacon API or a buffer
20
+ * waiting on a lazy resolver), not whether the server received it.
21
+ */
22
+ export type TrackResult = TrackSuccess | TrackFailure;
23
+ /**
24
+ * Ambient page/session metadata that decorates every event. Mirrors the Umami
25
+ * `/api/send` payload so transports can pass it through unchanged.
26
+ *
27
+ * Browser transports auto-populate these from `window`/`document`/`navigator`
28
+ * when fields are absent; pass overrides via {@link TrackOptions.context} for
29
+ * SSR, web workers, or tests.
30
+ */
31
+ export interface EventContext {
32
+ /** Page URL path + query, e.g. `/products/abc?ref=hero`. */
33
+ url?: string;
34
+ /** Document title. */
35
+ title?: string;
36
+ /** Hostname seen by the visitor, e.g. `shop.example.com`. */
37
+ hostname?: string;
38
+ /** HTTP `Referer` (sic) — origin page that linked to the current one. */
39
+ referrer?: string;
40
+ /** BCP-47 language tag, e.g. `en-US`. */
41
+ language?: string;
42
+ /** Screen size as `"<width>x<height>"`, e.g. `"1920x1080"`. */
43
+ screen?: string;
44
+ }
45
+ export interface TrackOptions {
46
+ /**
47
+ * Per-call overrides merged on top of whatever the transport auto-derives.
48
+ * Use this to attach SSR-known values or to scrub a field for a single event.
49
+ */
50
+ context?: EventContext;
51
+ }
52
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;CACV;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;AAEtD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB"}
@@ -0,0 +1,28 @@
1
+ import type { TrackOptions, TrackResult } from "./base.js";
2
+ import type { TrackingEvent } from "./events.js";
3
+ /** Npm package id. */
4
+ export declare const SDK_PACKAGE_ID: "@studio-vaai/js-tracking";
5
+ /** Marker config every transport extends. */
6
+ export type TrackingClientConfig = Record<never, never>;
7
+ /**
8
+ * Transport-agnostic event sender. Implementations live under `impl/` (e.g.
9
+ * the Umami beacon transport) — consumers depend on this interface so the
10
+ * underlying analytics backend can be swapped without touching call sites.
11
+ */
12
+ export interface TrackingClient {
13
+ /**
14
+ * Send a single typed event. Synchronous and fire-and-forget — implementations
15
+ * dispatch via the page-unload-safe Beacon API. Returns a {@link TrackResult}
16
+ * reflecting whether the event was successfully queued for delivery, NOT
17
+ * whether the server acknowledged it. Never throws — analytics must not
18
+ * break the host app.
19
+ *
20
+ * Buffered events (e.g. while a lazy resolver is in flight) return
21
+ * `{ ok: true }` immediately; if the eventual flush fails, the caller is
22
+ * not notified — that's the price of fire-and-forget analytics.
23
+ */
24
+ trackEvent(event: TrackingEvent, options?: TrackOptions): TrackResult;
25
+ }
26
+ /** Contract-only stub — throws; a transport package supplies a real implementation. */
27
+ export declare function createTrackingClient(_config: TrackingClientConfig): TrackingClient;
28
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,sBAAsB;AACtB,eAAO,MAAM,cAAc,EAAG,0BAAmC,CAAC;AAElE,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;CACvE;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CAElF"}
package/dist/client.js ADDED
@@ -0,0 +1,10 @@
1
+ import { TrackingClientNotImplementedError } from "./errors.js";
2
+ //#region src/client.ts
3
+ /** Npm package id. */
4
+ var SDK_PACKAGE_ID = "@studio-vaai/js-tracking";
5
+ /** Contract-only stub — throws; a transport package supplies a real implementation. */
6
+ function createTrackingClient(_config) {
7
+ throw new TrackingClientNotImplementedError();
8
+ }
9
+ //#endregion
10
+ export { SDK_PACKAGE_ID, createTrackingClient };
@@ -0,0 +1,6 @@
1
+ /** Thrown by the contract-only {@link createTrackingClient} stub. */
2
+ export declare class TrackingClientNotImplementedError extends Error {
3
+ readonly code: "tracking_client_not_implemented";
4
+ constructor();
5
+ }
6
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,IAAI,EAAG,iCAAiC,CAAU;IAE3D,cAKC;CACF"}
package/dist/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region src/errors.ts
2
+ /** Thrown by the contract-only {@link createTrackingClient} stub. */
3
+ var TrackingClientNotImplementedError = class extends Error {
4
+ code = "tracking_client_not_implemented";
5
+ constructor() {
6
+ super("@studio-vaai/js-tracking: createTrackingClient has no runtime implementation; use a transport like createFetchUmamiTrackingClient.");
7
+ this.name = "TrackingClientNotImplementedError";
8
+ }
9
+ };
10
+ //#endregion
11
+ export { TrackingClientNotImplementedError };
@@ -0,0 +1,127 @@
1
+ import type { Step } from "./steps.js";
2
+ /**
3
+ * Shape every tracked event must satisfy. The `name` field is the
4
+ * discriminator Umami uses for the event name; `data` becomes Umami's event
5
+ * `data` payload — each variant declares its own concrete data shape so call
6
+ * sites get field-level autocomplete.
7
+ *
8
+ * Wire convention: event names and `data` keys are kebab-case so they read
9
+ * cleanly in Umami's event-data viewer. Values must be string/number/boolean
10
+ * primitives — the transport doesn't recursively serialize nested objects.
11
+ */
12
+ export interface TrackingEventBase {
13
+ /** Stable event name (kebab-case). Becomes the Umami event name. */
14
+ readonly name: string;
15
+ /** Flat primitive payload. Each variant tightens this to a concrete shape. */
16
+ readonly data?: object;
17
+ }
18
+ /**
19
+ * Fields present on every event's `data`. The transport additionally injects
20
+ * `sdk-version` from {@link FetchUmamiTrackingClientConfig.sdkVersion}, so
21
+ * callers never set that themselves.
22
+ */
23
+ interface CommonData {
24
+ readonly "product-id": string;
25
+ }
26
+ interface EntrypointImpressionEvent extends TrackingEventBase {
27
+ readonly name: "entrypoint-impression";
28
+ readonly data: CommonData & {
29
+ /** Merchant-defined identifier for the entrypoint placement. */
30
+ readonly entry: string;
31
+ };
32
+ }
33
+ interface EntrypointClickedEvent extends TrackingEventBase {
34
+ readonly name: "entrypoint-clicked";
35
+ readonly data: CommonData & {
36
+ readonly entry: string;
37
+ /**
38
+ * Optional finer-grained placement classifier within `entry` (e.g.
39
+ * `entry: "pdp-button"` + `subentry: "size-row"`). Merchant-defined.
40
+ */
41
+ readonly subentry?: string;
42
+ readonly "current-step": Step;
43
+ };
44
+ }
45
+ interface IntroOpenedEvent extends TrackingEventBase {
46
+ readonly name: "intro-opened";
47
+ readonly data: CommonData & {
48
+ /** Typically `entry` | `image-rejection` | `image-record`. */
49
+ readonly source: Step;
50
+ };
51
+ }
52
+ interface ConsentGrantedEvent extends TrackingEventBase {
53
+ readonly name: "consent-granted";
54
+ readonly data: CommonData;
55
+ }
56
+ interface ImageRecordInitiatedEvent extends TrackingEventBase {
57
+ readonly name: "image-record-initiated";
58
+ readonly data: CommonData & {
59
+ /** Typically `intro` | `height`. */
60
+ readonly source: Step;
61
+ };
62
+ }
63
+ interface ImageUploadInitiatedEvent extends TrackingEventBase {
64
+ readonly name: "image-upload-initiated";
65
+ readonly data: CommonData & {
66
+ /** Typically `intro` | `height`. */
67
+ readonly source: Step;
68
+ };
69
+ }
70
+ interface ImageUploadAbortedEvent extends TrackingEventBase {
71
+ readonly name: "image-upload-aborted";
72
+ readonly data: CommonData;
73
+ }
74
+ interface ImageUploadedEvent extends TrackingEventBase {
75
+ readonly name: "image-uploaded";
76
+ readonly data: CommonData & {
77
+ readonly "image-source": "record-self-timer" | "record-system-dialog" | "select";
78
+ };
79
+ }
80
+ interface HeightEnteredEvent extends TrackingEventBase {
81
+ readonly name: "height-entered";
82
+ readonly data: CommonData & {
83
+ /** Typically `image-upload` | `variant-selection`. */
84
+ readonly source: Step;
85
+ };
86
+ }
87
+ interface VariantSelectionInitiatedEvent extends TrackingEventBase {
88
+ readonly name: "variant-selection-initiated";
89
+ readonly data: CommonData & {
90
+ /** Typically `height` | `try-on`. */
91
+ readonly source: Step;
92
+ };
93
+ }
94
+ /**
95
+ * Selected variant axes carried by events that fire after the user has chosen
96
+ * a specific variant (try-on, cart add, order). Emitted as flat
97
+ * `variant-<axis>` keys (e.g. `variant-color`, `variant-size`,
98
+ * `variant-length`). Axis names are open so merchants can configure new axes
99
+ * without an SDK change.
100
+ */
101
+ type VariantData = Readonly<Partial<Record<`variant-${string}`, string>>>;
102
+ interface TryOnInitiatedEvent extends TrackingEventBase {
103
+ readonly name: "try-on-initiated";
104
+ readonly data: CommonData & VariantData;
105
+ }
106
+ interface CartItemAddedEvent extends TrackingEventBase {
107
+ readonly name: "cart-item-added";
108
+ readonly data: CommonData & VariantData;
109
+ }
110
+ interface OrderPlacedEvent extends TrackingEventBase {
111
+ readonly name: "order-placed";
112
+ readonly data: CommonData & VariantData;
113
+ }
114
+ interface ViewClosedEvent extends TrackingEventBase {
115
+ readonly name: "view-closed";
116
+ readonly data: CommonData & {
117
+ readonly "current-step": Step;
118
+ };
119
+ }
120
+ /**
121
+ * Discriminated union of every event the SDK can send. Add new events by
122
+ * declaring a variant interface above and appending it to the union — call
123
+ * sites get full autocomplete + type-checking on `name` and `data`.
124
+ */
125
+ export type TrackingEvent = EntrypointImpressionEvent | EntrypointClickedEvent | IntroOpenedEvent | ConsentGrantedEvent | ImageRecordInitiatedEvent | ImageUploadInitiatedEvent | ImageUploadAbortedEvent | ImageUploadedEvent | HeightEnteredEvent | VariantSelectionInitiatedEvent | TryOnInitiatedEvent | CartItemAddedEvent | OrderPlacedEvent | ViewClosedEvent;
126
+ export {};
127
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,UAAU,UAAU;IAClB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,UAAU,yBAA0B,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,gEAAgE;QAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,UAAU,sBAAuB,SAAQ,iBAAiB;IACxD,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB;;;WAGG;QACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;KAC/B,CAAC;CACH;AAED,UAAU,gBAAiB,SAAQ,iBAAiB;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,8DAA8D;QAC9D,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;KACvB,CAAC;CACH;AAED,UAAU,mBAAoB,SAAQ,iBAAiB;IACrD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,UAAU,yBAA0B,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,oCAAoC;QACpC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;KACvB,CAAC;CACH;AAED,UAAU,yBAA0B,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,oCAAoC;QACpC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;KACvB,CAAC;CACH;AAED,UAAU,uBAAwB,SAAQ,iBAAiB;IACzD,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,UAAU,kBAAmB,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,QAAQ,CAAC,cAAc,EAAE,mBAAmB,GAAG,sBAAsB,GAAG,QAAQ,CAAC;KAClF,CAAC;CACH;AAED,UAAU,kBAAmB,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,sDAAsD;QACtD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;KACvB,CAAC;CACH;AAED,UAAU,8BAA+B,SAAQ,iBAAiB;IAChE,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,qCAAqC;QACrC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;KACvB,CAAC;CACH;AAED;;;;;;GAMG;AACH,KAAK,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAE1E,UAAU,mBAAoB,SAAQ,iBAAiB;IACrD,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CACzC;AAED,UAAU,kBAAmB,SAAQ,iBAAiB;IACpD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CACzC;AAED,UAAU,gBAAiB,SAAQ,iBAAiB;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;CACzC;AAED,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG;QAC1B,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC;KAC/B,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACrB,yBAAyB,GACzB,sBAAsB,GACtB,gBAAgB,GAChB,mBAAmB,GACnB,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,kBAAkB,GAClB,kBAAkB,GAClB,8BAA8B,GAC9B,mBAAmB,GACnB,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,CAAC"}
@@ -0,0 +1,69 @@
1
+ import type { EventContext } from "../base.js";
2
+ import type { TrackingClient, TrackingClientConfig } from "../client.js";
3
+ /** Default Umami base URL (int/prod Umami). */
4
+ export declare const DEFAULT_UMAMI_BASE_URL: "https://a.studio-vaai.com";
5
+ /** Per-shop Umami config the {@link FetchUmamiTrackingClientConfig.resolveConfig} resolver returns. */
6
+ export interface ResolvedUmamiConfig {
7
+ /** Umami website UUID for this shop. */
8
+ websiteId: string;
9
+ /** Optional override; defaults to {@link DEFAULT_UMAMI_BASE_URL} when omitted. */
10
+ baseUrl?: string;
11
+ }
12
+ /**
13
+ * Sync sender used by the transport. The default delegates to
14
+ * `navigator.sendBeacon` — the page-unload-safe analytics primitive. Tests
15
+ * inject their own recorder.
16
+ */
17
+ export type SendBeaconFn = (url: string, data: BodyInit) => boolean;
18
+ export interface FetchUmamiTrackingClientConfig extends TrackingClientConfig {
19
+ /**
20
+ * Stamped on every event as the `sdk-version` field. Higher SDK layers
21
+ * (e.g. `@studio-vaai/react-components`) pass their own
22
+ * `"<pkg>@<version>"` here so the analytics view shows the version of the
23
+ * SDK merchants actually install — not this internal tracking package.
24
+ */
25
+ sdkVersion: string;
26
+ /**
27
+ * Static Umami website UUID. Provide this OR {@link resolveConfig}, not both.
28
+ * Use the static form when the website id is known at client construction.
29
+ */
30
+ websiteId?: string;
31
+ /** Static Umami base URL. Used together with {@link websiteId}. Defaults to {@link DEFAULT_UMAMI_BASE_URL}. */
32
+ baseUrl?: string;
33
+ /**
34
+ * Lazy resolver for per-shop Umami config. Provide this OR {@link websiteId},
35
+ * not both. Runs once when the client is constructed; events fired before
36
+ * the promise settles are buffered internally and flushed (in order) the
37
+ * moment the resolver returns. Buffered events have already returned
38
+ * `{ ok: true }` to the caller — if the eventual flush fails (resolver
39
+ * rejected or sendBeacon refused) the caller is not notified.
40
+ */
41
+ resolveConfig?: () => Promise<ResolvedUmamiConfig>;
42
+ /**
43
+ * Override the beacon sender. Defaults to `navigator.sendBeacon` bound to
44
+ * `navigator` when available; falls back to a no-op that reports `false`
45
+ * (so the call site can surface `{ ok: false }` in SSR / older browsers).
46
+ */
47
+ sendBeacon?: SendBeaconFn;
48
+ /**
49
+ * Ambient context applied to every payload, before per-call overrides. Useful
50
+ * for SSR-known fields or to pin a hostname different from `window.location`.
51
+ */
52
+ defaultContext?: EventContext;
53
+ }
54
+ /**
55
+ * Umami `/api/send` transport for {@link TrackingClient}. Dispatches via the
56
+ * Beacon API so events survive page unload and navigation; the return value
57
+ * reports whether the browser accepted the beacon, not whether the server
58
+ * acknowledged it. Errors are never thrown — analytics cannot break the host app.
59
+ *
60
+ * Supports two configuration modes:
61
+ * - **Static**: pass `websiteId` (and optionally `baseUrl`) at construction.
62
+ * Events fire immediately.
63
+ * - **Lazy**: pass `resolveConfig` for per-shop Umami config that's only known
64
+ * asynchronously (e.g. fetched via `client.tracking.getTracking()`). Events
65
+ * fired before the resolver settles are buffered in order and flushed once
66
+ * the config is available.
67
+ */
68
+ export declare function createFetchUmamiTrackingClient(config: FetchUmamiTrackingClientConfig): TrackingClient;
69
+ //# sourceMappingURL=fetch-umami-tracking-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-umami-tracking-client.d.ts","sourceRoot":"","sources":["../../src/impl/fetch-umami-tracking-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAyC,MAAM,YAAY,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzE,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,EAAG,2BAAoC,CAAC;AAE3E,uGAAuG;AACvG,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;AAEpE,MAAM,WAAW,8BAA+B,SAAQ,oBAAoB;IAC1E;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+GAA+G;IAC/G,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnD;;;;OAIG;IACH,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,YAAY,CAAC;CAC/B;AAkHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,8BAA8B,GACrC,cAAc,CAgJhB"}
@@ -0,0 +1,154 @@
1
+ //#region src/impl/fetch-umami-tracking-client.ts
2
+ /** Default Umami base URL (int/prod Umami). */
3
+ var DEFAULT_UMAMI_BASE_URL = "https://a.studio-vaai.com";
4
+ function deriveBrowserContext() {
5
+ const ctx = {};
6
+ if (typeof globalThis === "undefined") return ctx;
7
+ const w = globalThis.window;
8
+ if (typeof w === "undefined") return ctx;
9
+ const loc = w.location;
10
+ if (typeof loc !== "undefined") {
11
+ ctx.hostname = loc.hostname;
12
+ ctx.url = `${loc.pathname}${loc.search}`;
13
+ }
14
+ const doc = w.document;
15
+ if (typeof doc !== "undefined") {
16
+ ctx.title = doc.title;
17
+ if (doc.referrer !== "") ctx.referrer = doc.referrer;
18
+ }
19
+ const nav = w.navigator;
20
+ if (typeof nav !== "undefined") ctx.language = nav.language;
21
+ const scr = w.screen;
22
+ if (typeof scr !== "undefined") ctx.screen = `${String(scr.width)}x${String(scr.height)}`;
23
+ return ctx;
24
+ }
25
+ function mergeContext(base, defaults, overrides) {
26
+ return {
27
+ ...base,
28
+ ...defaults,
29
+ ...overrides
30
+ };
31
+ }
32
+ function applyContext(payload, ctx) {
33
+ if (typeof ctx.hostname !== "undefined") payload.hostname = ctx.hostname;
34
+ if (typeof ctx.language !== "undefined") payload.language = ctx.language;
35
+ if (typeof ctx.screen !== "undefined") payload.screen = ctx.screen;
36
+ if (typeof ctx.title !== "undefined") payload.title = ctx.title;
37
+ if (typeof ctx.url !== "undefined") payload.url = ctx.url;
38
+ if (typeof ctx.referrer !== "undefined") payload.referrer = ctx.referrer;
39
+ }
40
+ function networkError(message, details) {
41
+ return details === void 0 ? {
42
+ kind: "network",
43
+ message
44
+ } : {
45
+ kind: "network",
46
+ message,
47
+ details
48
+ };
49
+ }
50
+ function bakeState(websiteId, baseUrl) {
51
+ return {
52
+ endpoint: `${(baseUrl ?? "https://a.studio-vaai.com").replace(/\/$/, "")}/api/send`,
53
+ websiteId
54
+ };
55
+ }
56
+ function defaultSendBeacon() {
57
+ return (url, data) => {
58
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
59
+ return navigator.sendBeacon.bind(navigator)(url, data);
60
+ };
61
+ }
62
+ /**
63
+ * Umami `/api/send` transport for {@link TrackingClient}. Dispatches via the
64
+ * Beacon API so events survive page unload and navigation; the return value
65
+ * reports whether the browser accepted the beacon, not whether the server
66
+ * acknowledged it. Errors are never thrown — analytics cannot break the host app.
67
+ *
68
+ * Supports two configuration modes:
69
+ * - **Static**: pass `websiteId` (and optionally `baseUrl`) at construction.
70
+ * Events fire immediately.
71
+ * - **Lazy**: pass `resolveConfig` for per-shop Umami config that's only known
72
+ * asynchronously (e.g. fetched via `client.tracking.getTracking()`). Events
73
+ * fired before the resolver settles are buffered in order and flushed once
74
+ * the config is available.
75
+ */
76
+ function createFetchUmamiTrackingClient(config) {
77
+ const sendBeacon = config.sendBeacon ?? defaultSendBeacon();
78
+ let resolved = null;
79
+ let resolverError = null;
80
+ const pending = [];
81
+ const staticWebsiteId = config.websiteId;
82
+ const resolverFn = config.resolveConfig;
83
+ if (staticWebsiteId === void 0 && resolverFn === void 0) throw new Error("createFetchUmamiTrackingClient: provide either `websiteId` or `resolveConfig`");
84
+ if (staticWebsiteId !== void 0 && resolverFn !== void 0) throw new Error("createFetchUmamiTrackingClient: pass either `websiteId` or `resolveConfig`, not both");
85
+ if (staticWebsiteId !== void 0) resolved = bakeState(staticWebsiteId, config.baseUrl);
86
+ else if (resolverFn !== void 0) {
87
+ if (config.baseUrl !== void 0) throw new Error("createFetchUmamiTrackingClient: `baseUrl` is set by the resolver in lazy mode — drop the static `baseUrl`");
88
+ (async () => {
89
+ try {
90
+ const r = await resolverFn();
91
+ resolved = bakeState(r.websiteId, r.baseUrl);
92
+ } catch (error) {
93
+ resolverError = {
94
+ kind: "unknown",
95
+ message: "Failed to resolve Umami tracking config",
96
+ details: error
97
+ };
98
+ }
99
+ while (pending.length > 0) {
100
+ const item = pending.shift();
101
+ if (item === void 0) break;
102
+ if (resolved === null) continue;
103
+ sendNow(resolved, item.event, item.options);
104
+ }
105
+ })();
106
+ }
107
+ function dispatchBeacon(state, body) {
108
+ let blob;
109
+ try {
110
+ blob = new Blob([JSON.stringify(body)], { type: "application/json" });
111
+ } catch (error) {
112
+ return {
113
+ ok: false,
114
+ error: networkError("Failed to serialise event", error)
115
+ };
116
+ }
117
+ if (!sendBeacon(state.endpoint, blob)) return {
118
+ ok: false,
119
+ error: networkError("navigator.sendBeacon refused the event (quota exceeded, payload too large, or unavailable)")
120
+ };
121
+ return { ok: true };
122
+ }
123
+ function track(state, name, data, contextOverrides) {
124
+ const payload = { website: state.websiteId };
125
+ applyContext(payload, mergeContext(deriveBrowserContext(), config.defaultContext, contextOverrides));
126
+ if (typeof name !== "undefined") payload.name = name;
127
+ if (typeof data !== "undefined") payload.data = { ...data };
128
+ return dispatchBeacon(state, {
129
+ type: "event",
130
+ payload
131
+ });
132
+ }
133
+ function sendNow(state, event, options) {
134
+ const data = {
135
+ ...event.data ?? {},
136
+ "sdk-version": config.sdkVersion
137
+ };
138
+ return track(state, event.name, data, options?.context);
139
+ }
140
+ return { trackEvent: (event, options) => {
141
+ if (resolved !== null) return sendNow(resolved, event, options);
142
+ if (resolverError !== null) return {
143
+ ok: false,
144
+ error: resolverError
145
+ };
146
+ pending.push({
147
+ event,
148
+ options
149
+ });
150
+ return { ok: true };
151
+ } };
152
+ }
153
+ //#endregion
154
+ export { DEFAULT_UMAMI_BASE_URL, createFetchUmamiTrackingClient };
@@ -0,0 +1,9 @@
1
+ export type { EventContext, TrackError, TrackErrorKind, TrackFailure, TrackOptions, TrackResult, TrackSuccess, } from "./base.js";
2
+ export type { TrackingClient, TrackingClientConfig } from "./client.js";
3
+ export { createTrackingClient, SDK_PACKAGE_ID } from "./client.js";
4
+ export { TrackingClientNotImplementedError } from "./errors.js";
5
+ export type { TrackingEvent, TrackingEventBase } from "./events.js";
6
+ export type { Step } from "./steps.js";
7
+ export type { FetchUmamiTrackingClientConfig, ResolvedUmamiConfig, } from "./impl/fetch-umami-tracking-client.js";
8
+ export { createFetchUmamiTrackingClient, DEFAULT_UMAMI_BASE_URL, } from "./impl/fetch-umami-tracking-client.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,iCAAiC,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,YAAY,EACV,8BAA8B,EAC9B,mBAAmB,GACpB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,8BAA8B,EAC9B,sBAAsB,GACvB,MAAM,uCAAuC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { TrackingClientNotImplementedError } from "./errors.js";
2
+ import { SDK_PACKAGE_ID, createTrackingClient } from "./client.js";
3
+ import { DEFAULT_UMAMI_BASE_URL, createFetchUmamiTrackingClient } from "./impl/fetch-umami-tracking-client.js";
4
+ export { DEFAULT_UMAMI_BASE_URL, SDK_PACKAGE_ID, TrackingClientNotImplementedError, createFetchUmamiTrackingClient, createTrackingClient };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Every distinct step in the studio vaai try-on flow state machine. Used as
3
+ * the `source` and `current-step` value type on tracking events.
4
+ *
5
+ * This union is the analytics view of the flow — it doesn't have to match the
6
+ * exact XState state IDs in `@studio-vaai/js-machine` 1:1. Callers translate
7
+ * from machine state to {@link Step} at the event-emit site. Extend the union
8
+ * here when a new flow step needs to appear in analytics.
9
+ */
10
+ export type Step = "entry" | "intro" | "consent" | "image-record" | "image-upload" | "image-rejection" | "height" | "variant-selection" | "try-on" | "cart" | "order";
11
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,IAAI,GACZ,OAAO,GACP,OAAO,GACP,SAAS,GACT,cAAc,GACd,cAAc,GACd,iBAAiB,GACjB,QAAQ,GACR,mBAAmB,GACnB,QAAQ,GACR,MAAM,GACN,OAAO,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@studio-vaai/js-tracking",
3
+ "version": "0.1.3",
4
+ "description": "Typed analytics events for studio vaai, plus a fetch-based Umami transport.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "vite build --config vite.lib.config.ts && rm -f tsconfig.tsbuildinfo && tsgo --emitDeclarationOnly",
21
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
22
+ "test": "vitest run --config ../../vitest.config.ts",
23
+ "test:watch": "vitest --config ../../vitest.config.ts"
24
+ },
25
+ "devDependencies": {
26
+ "vite": "8.0.5"
27
+ }
28
+ }