@escape-game-over/atlas 0.1.64 → 0.1.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -14,8 +14,13 @@ type ConsentGlobal = {
14
14
  ) => void;
15
15
  };
16
16
 
17
+ // `googleEvent`'s way in, under the same arrangement.
18
+ type EventGlobal = {
19
+ [Name in typeof import("./google.ts").GOOGLE_EVENT_GLOBAL]?: import("./google.ts").GoogleEventSink;
20
+ };
21
+
17
22
  // biome-ignore lint/correctness/noUnusedVariables: merges into the global `Window`.
18
- interface Window extends ConsentGlobal {
23
+ interface Window extends ConsentGlobal, EventGlobal {
19
24
  dataLayer: unknown[];
20
25
  gtag: (...args: unknown[]) => void;
21
26
  }
@@ -86,6 +91,24 @@ interface Window extends ConsentGlobal {
86
91
 
87
92
  defineGtag();
88
93
 
94
+ // An event, wherever this deployment's settings send one. Tag Manager
95
+ // reads a pushed object rather than a `gtag` call, and clears the previous
96
+ // `ecommerce` only when told to, as Google documents.
97
+ window.__googleEvent = (name, params, ecommerce) => {
98
+ if (config.tagIds.length > 0) {
99
+ if (params === undefined) window.gtag("event", name);
100
+ else window.gtag("event", name, params);
101
+ }
102
+ if (config.containerIds.length > 0) {
103
+ if (ecommerce) {
104
+ window.dataLayer.push({ ecommerce: null });
105
+ window.dataLayer.push({ event: name, ecommerce: params });
106
+ } else {
107
+ window.dataLayer.push({ event: name, ...params });
108
+ }
109
+ }
110
+ };
111
+
89
112
  // Ahead of everything, which is the entire point of emitting this here.
90
113
  for (const given of config.defaults) {
91
114
  window.gtag("consent", "default", given);
@@ -217,6 +217,28 @@ const CONSENT_KEYS = {
217
217
  */
218
218
  export const CONSENT_UPDATE_GLOBAL = "__consent";
219
219
 
220
+ /**
221
+ * The global `googleEvent` calls to record an event:
222
+ * `__googleEvent("generate_lead", { value: 40 }, true)`.
223
+ *
224
+ * A global for the reason `CONSENT_UPDATE_GLOBAL` is one, and emitted here
225
+ * because only this side knows where an event goes: to `gtag.js` when there
226
+ * are tag ids, onto `dataLayer` as a Tag Manager event when there are
227
+ * containers. A client script deciding that would be a second copy of the
228
+ * settings.
229
+ */
230
+ export const GOOGLE_EVENT_GLOBAL = "__googleEvent";
231
+
232
+ /**
233
+ * What `GOOGLE_EVENT_GLOBAL` holds. `ecommerce` says the parameters go under
234
+ * Tag Manager's `ecommerce` key, where its GA4 tags read items and revenue.
235
+ */
236
+ export type GoogleEventSink = (
237
+ name: string,
238
+ params: object | undefined,
239
+ ecommerce: boolean
240
+ ) => void;
241
+
220
242
  /** The signal keys of a set of defaults — not `region`, not `waitForUpdate`. */
221
243
  const SIGNAL_KEYS = [
222
244
  "adStorage",
@@ -36,7 +36,16 @@ export interface UmamiTracker {
36
36
  readonly performance?: boolean;
37
37
  /** Leave `?query` out of recorded URLs. */
38
38
  readonly excludeSearch?: boolean;
39
- /** Leave `#hash` out of recorded URLs. */
39
+ /**
40
+ * Leave `#hash` out of recorded URLs. **Defaults to on here.**
41
+ *
42
+ * Umami's own default is off, and the tracker counts a pageview whenever
43
+ * the URL it compares changes. So anything on the page that rewrites the
44
+ * fragment (a gallery, a tab, an anchor link) is recorded as another visit
45
+ * to a page nobody left.
46
+ *
47
+ * `false` turns it off, and writes the word rather than omitting it.
48
+ */
40
49
  readonly excludeHash?: boolean;
41
50
  /** Honour the browser's Do Not Track setting. */
42
51
  readonly doNotTrack?: boolean;
@@ -267,15 +276,15 @@ export function umamiScripts(
267
276
  src: at(umami.tracker?.path ?? "/script.js"),
268
277
  attrs: stated({
269
278
  ...common,
270
- // The one default lib overrides — see `UmamiTracker`. The
279
+ // The two defaults lib overrides — see `UmamiTracker`. The
271
280
  // others are left to Umami: `auto-track` and `auto-pageview`
272
281
  // are already on unless the word `false` appears, so stating
273
282
  // them would be bytes that change nothing, and whether to
274
283
  // honour Do Not Track or drop query strings is a decision about
275
284
  // this business rather than about this kind of site.
276
285
  "data-performance": flag(umami.tracker?.performance ?? true),
286
+ "data-exclude-hash": flag(umami.tracker?.excludeHash ?? true),
277
287
  "data-exclude-search": flag(umami.tracker?.excludeSearch),
278
- "data-exclude-hash": flag(umami.tracker?.excludeHash),
279
288
  "data-do-not-track": flag(umami.tracker?.doNotTrack),
280
289
  "data-auto-track": flag(umami.tracker?.autoTrack),
281
290
  "data-auto-pageview": flag(umami.tracker?.autoPageview),
@@ -1,3 +1,21 @@
1
+ import type { DeclaredEvent } from "./event-data.ts";
2
+
3
+ /**
4
+ * The project's Clarity events. Clarity carries a name and nothing else, so
5
+ * each is declared `undefined`. Nothing compiles until a project declares its
6
+ * own, once:
7
+ *
8
+ * ```ts
9
+ * declare module "@escape-game-over/atlas/client" {
10
+ * interface ClarityEvents {
11
+ * "form sent": undefined;
12
+ * }
13
+ * }
14
+ * ```
15
+ */
16
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
17
+ export interface ClarityEvents {}
18
+
1
19
  /** What `clarity.ts` defines once the page has loaded. */
2
20
  interface ClarityWindow {
3
21
  clarity?: (...args: unknown[]) => void;
@@ -10,6 +28,6 @@ interface ClarityWindow {
10
28
  * Sent with or without consent: Clarity records either way, and without it
11
29
  * sets no cookie. Dropped before Clarity loads, or where it is not configured.
12
30
  */
13
- export function clarityEvent(name: string): void {
31
+ export function clarityEvent(name: DeclaredEvent<ClarityEvents>): void {
14
32
  (window as unknown as ClarityWindow).clarity?.("event", name);
15
33
  }
@@ -26,6 +26,7 @@ export {
26
26
  } from "./element.ts";
27
27
  export * from "./filters.ts";
28
28
  export * from "./filters-view.ts";
29
+ export * from "./google-event.ts";
29
30
  export * from "./meta-event.ts";
30
31
  export { data, ref, refs } from "./ref.ts";
31
32
  export * from "./snap-event.ts";
@@ -1,4 +1,21 @@
1
1
  import { readConsent } from "./consent.ts";
2
+ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
3
+
4
+ /**
5
+ * The project's Drip events — the actions its workflows start on — each with
6
+ * the properties it carries, `undefined` for none. Drip's events are named by
7
+ * the site, so nothing compiles until a project declares its own, once:
8
+ *
9
+ * ```ts
10
+ * declare module "@escape-game-over/atlas/client" {
11
+ * interface DripEvents {
12
+ * "Bought a voucher": { readonly value: number };
13
+ * }
14
+ * }
15
+ * ```
16
+ */
17
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
18
+ export interface DripEvents {}
2
19
 
3
20
  /** What `drip.ts` defines once marketing is granted. */
4
21
  interface DripWindow {
@@ -7,15 +24,18 @@ interface DripWindow {
7
24
 
8
25
  /**
9
26
  * Records an event with Drip, to start a workflow — a lead sent, a voucher
10
- * bought. Drip's events are named by the site, so any name goes.
27
+ * bought.
11
28
  *
12
29
  * Dropped unless Drip loaded and marketing is still granted: Drip has no
13
30
  * consent call of its own, so the check is here. An analytics call must never
14
31
  * be the reason a page misbehaves, so a dropped event is silent.
15
32
  */
16
- export function dripEvent(
17
- action: string,
18
- properties?: Readonly<Record<string, string | number | boolean>>
33
+ export function dripEvent<E extends DeclaredEvent<DripEvents>>(
34
+ action: E,
35
+ // Drip takes flat values; a nested object in a declaration fails here.
36
+ ...[properties]: EventDataArgs<
37
+ DripEvents[E] & Readonly<Record<string, string | number | boolean>>
38
+ >
19
39
  ): void {
20
40
  if (readConsent()?.marketing !== "granted") return;
21
41
  const queue = (window as unknown as DripWindow)._dcq;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The arguments an event's declared data asks for: none when it is
3
+ * `undefined`, optional when every field is, required otherwise.
4
+ *
5
+ * Shared by every vendor whose events a project declares for itself, so
6
+ * `{ form_sent: undefined }` means the same thing in each.
7
+ */
8
+ export type EventDataArgs<Data> = [Data] extends [undefined]
9
+ ? []
10
+ : Partial<Data> extends Data
11
+ ? [data?: Data]
12
+ : [data: Data];
13
+
14
+ /** The event names a project declared, as the strings they are sent as. */
15
+ export type DeclaredEvent<Declared> = Extract<keyof Declared, string>;
@@ -0,0 +1,119 @@
1
+ import {
2
+ GOOGLE_EVENT_GLOBAL,
3
+ type GoogleEventSink,
4
+ } from "../analytics/google.ts";
5
+ import type { CurrencyCode } from "../money.ts";
6
+ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
7
+
8
+ /** GA4's recommended events that carry items and revenue. */
9
+ const ECOMMERCE_EVENTS = [
10
+ "add_payment_info",
11
+ "add_to_cart",
12
+ "add_to_wishlist",
13
+ "begin_checkout",
14
+ "purchase",
15
+ "select_item",
16
+ "select_promotion",
17
+ "view_cart",
18
+ "view_item",
19
+ "view_item_list",
20
+ "view_promotion",
21
+ ] as const;
22
+
23
+ /** GA4's recommended events that a website sends, which its reports know by name. */
24
+ export type GoogleRecommendedEvent =
25
+ | (typeof ECOMMERCE_EVENTS)[number]
26
+ | "generate_lead"
27
+ | "login"
28
+ | "search"
29
+ | "select_content"
30
+ | "share"
31
+ | "sign_up";
32
+
33
+ /**
34
+ * The project's own events — what its Tag Manager triggers listen for — each
35
+ * with the data it carries, `undefined` for none. Empty here; a project fills
36
+ * it once, and a name it did not declare does not compile:
37
+ *
38
+ * ```ts
39
+ * declare module "@escape-game-over/atlas/client" {
40
+ * interface GoogleCustomEvents {
41
+ * form_submission_contact: undefined;
42
+ * form_submission_quote: { readonly room: string };
43
+ * }
44
+ * }
45
+ * ```
46
+ */
47
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
48
+ export interface GoogleCustomEvents {}
49
+
50
+ /** A recommended event, or one the project declared. */
51
+ export type GoogleEventName =
52
+ | GoogleRecommendedEvent
53
+ | DeclaredEvent<GoogleCustomEvents>;
54
+
55
+ /** One item of an ecommerce event. Google needs its id or its name. */
56
+ export type GoogleItem = (
57
+ | { readonly item_id: string; readonly item_name?: string }
58
+ | { readonly item_id?: string; readonly item_name: string }
59
+ ) & {
60
+ readonly item_category?: string;
61
+ readonly item_variant?: string;
62
+ readonly price?: number;
63
+ readonly quantity?: number;
64
+ readonly coupon?: string;
65
+ };
66
+
67
+ /** GA4's standard parameters. What the visitor chose, never who they are. */
68
+ export interface GoogleEventData {
69
+ readonly value?: number;
70
+ readonly currency?: CurrencyCode;
71
+ readonly transaction_id?: string;
72
+ readonly coupon?: string;
73
+ readonly items?: readonly GoogleItem[];
74
+ readonly search_term?: string;
75
+ readonly method?: string;
76
+ readonly content_type?: string;
77
+ readonly content_id?: string;
78
+ }
79
+
80
+ /**
81
+ * A recommended event takes GA4's parameters; `purchase` without its three
82
+ * reports a sale of nothing, possibly twice. A declared one takes what the
83
+ * project declared.
84
+ */
85
+ type GoogleEventArgs<E extends GoogleEventName> =
86
+ E extends GoogleRecommendedEvent
87
+ ? E extends "purchase"
88
+ ? [
89
+ data: GoogleEventData & {
90
+ value: number;
91
+ currency: CurrencyCode;
92
+ transaction_id: string;
93
+ },
94
+ ]
95
+ : [data?: GoogleEventData]
96
+ : EventDataArgs<GoogleCustomEvents[E & keyof GoogleCustomEvents]>;
97
+
98
+ /**
99
+ * Records an event with Google — a lead sent, a booking made — through
100
+ * `gtag.js`, Tag Manager, or both, as the deployment's `google` settings say.
101
+ *
102
+ * Sent whatever the visitor answered: Consent Mode is how Google is told, and
103
+ * it already holds back what was not granted. Dropped where Google is not
104
+ * configured, since an analytics call must never be the reason a page
105
+ * misbehaves.
106
+ */
107
+ export function googleEvent<E extends GoogleEventName>(
108
+ event: E,
109
+ ...[data]: GoogleEventArgs<E>
110
+ ): void {
111
+ const send = (
112
+ window as unknown as Record<string, GoogleEventSink | undefined>
113
+ )[GOOGLE_EVENT_GLOBAL];
114
+ send?.(
115
+ event,
116
+ data,
117
+ (ECOMMERCE_EVENTS as readonly string[]).includes(event)
118
+ );
119
+ }
@@ -1,24 +1,46 @@
1
1
  import type { CurrencyCode } from "../money.ts";
2
+ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
2
3
 
3
4
  /** Meta's standard events, which its ad optimisation understands by name. */
4
- export type MetaStandardEvent =
5
- | "AddPaymentInfo"
6
- | "AddToCart"
7
- | "AddToWishlist"
8
- | "CompleteRegistration"
9
- | "Contact"
10
- | "CustomizeProduct"
11
- | "Donate"
12
- | "FindLocation"
13
- | "InitiateCheckout"
14
- | "Lead"
15
- | "Purchase"
16
- | "Schedule"
17
- | "Search"
18
- | "StartTrial"
19
- | "SubmitApplication"
20
- | "Subscribe"
21
- | "ViewContent";
5
+ const META_STANDARD_EVENTS = [
6
+ "AddPaymentInfo",
7
+ "AddToCart",
8
+ "AddToWishlist",
9
+ "CompleteRegistration",
10
+ "Contact",
11
+ "CustomizeProduct",
12
+ "Donate",
13
+ "FindLocation",
14
+ "InitiateCheckout",
15
+ "Lead",
16
+ "Purchase",
17
+ "Schedule",
18
+ "Search",
19
+ "StartTrial",
20
+ "SubmitApplication",
21
+ "Subscribe",
22
+ "ViewContent",
23
+ ] as const;
24
+
25
+ export type MetaStandardEvent = (typeof META_STANDARD_EVENTS)[number];
26
+
27
+ /**
28
+ * The project's own events, sent as Meta's custom events, each with the data
29
+ * it carries, `undefined` for none. Empty here; a project fills it once:
30
+ *
31
+ * ```ts
32
+ * declare module "@escape-game-over/atlas/client" {
33
+ * interface MetaCustomEvents {
34
+ * VoucherViewed: undefined;
35
+ * }
36
+ * }
37
+ * ```
38
+ */
39
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
40
+ export interface MetaCustomEvents {}
41
+
42
+ /** A standard event, or one the project declared. */
43
+ export type MetaEventName = MetaStandardEvent | DeclaredEvent<MetaCustomEvents>;
22
44
 
23
45
  /** Meta's standard parameters. What the visitor chose, never who they are. */
24
46
  export interface MetaEventData {
@@ -33,10 +55,15 @@ export interface MetaEventData {
33
55
  readonly status?: boolean;
34
56
  }
35
57
 
36
- /** `Purchase` is the one event Meta rejects without a value and currency. */
37
- type MetaEventArgs<E extends MetaStandardEvent> = E extends "Purchase"
38
- ? [data: MetaEventData & { value: number; currency: CurrencyCode }]
39
- : [data?: MetaEventData];
58
+ /**
59
+ * A standard event takes Meta's parameters; `Purchase` is the one it rejects
60
+ * without a value and currency. A declared one takes what the project declared.
61
+ */
62
+ type MetaEventArgs<E extends MetaEventName> = E extends MetaStandardEvent
63
+ ? E extends "Purchase"
64
+ ? [data: MetaEventData & { value: number; currency: CurrencyCode }]
65
+ : [data?: MetaEventData]
66
+ : EventDataArgs<MetaCustomEvents[E & keyof MetaCustomEvents]>;
40
67
 
41
68
  /** What `meta-pixel.ts` defines once marketing is granted. */
42
69
  interface MetaWindow {
@@ -44,18 +71,22 @@ interface MetaWindow {
44
71
  }
45
72
 
46
73
  /**
47
- * Records a standard event with Meta's pixel — a lead sent, a booking made.
74
+ * Records an event with Meta's pixel — a lead sent, a booking made. A declared
75
+ * event is sent as Meta's custom event, which is how Meta tells them apart.
48
76
  *
49
77
  * `fbq` exists only once the visitor granted marketing and the pixel loaded.
50
78
  * Before that, without consent, or where Meta is not configured, the event is
51
79
  * dropped: no consent means nothing to send, and an analytics call must never
52
80
  * be the reason a page misbehaves.
53
81
  */
54
- export function metaEvent<E extends MetaStandardEvent>(
82
+ export function metaEvent<E extends MetaEventName>(
55
83
  event: E,
56
84
  ...[data]: MetaEventArgs<E>
57
85
  ): void {
58
86
  const fbq = (window as unknown as MetaWindow).fbq;
59
- if (data === undefined) fbq?.("track", event);
60
- else fbq?.("track", event, data);
87
+ const method = (META_STANDARD_EVENTS as readonly string[]).includes(event)
88
+ ? "track"
89
+ : "trackCustom";
90
+ if (data === undefined) fbq?.(method, event);
91
+ else fbq?.(method, event, data);
61
92
  }
@@ -1,4 +1,5 @@
1
1
  import type { CurrencyCode } from "../money.ts";
2
+ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
2
3
 
3
4
  /** TikTok's standard events, by their current names (May 2025 onwards). */
4
5
  export type TikTokStandardEvent =
@@ -18,6 +19,26 @@ export type TikTokStandardEvent =
18
19
  | "Subscribe"
19
20
  | "ViewContent";
20
21
 
22
+ /**
23
+ * The project's own events, each with the data it carries, `undefined` for
24
+ * none. Empty here; a project fills it once:
25
+ *
26
+ * ```ts
27
+ * declare module "@escape-game-over/atlas/client" {
28
+ * interface TikTokCustomEvents {
29
+ * VoucherViewed: undefined;
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
35
+ export interface TikTokCustomEvents {}
36
+
37
+ /** A standard event, or one the project declared. */
38
+ export type TikTokEventName =
39
+ | TikTokStandardEvent
40
+ | DeclaredEvent<TikTokCustomEvents>;
41
+
21
42
  /** TikTok's standard parameters. What the visitor chose, never who they are. */
22
43
  export interface TikTokEventData {
23
44
  readonly value?: number;
@@ -31,10 +52,15 @@ export interface TikTokEventData {
31
52
  readonly description?: string;
32
53
  }
33
54
 
34
- /** `Purchase` needs a value and currency to be worth anything to TikTok. */
35
- type TikTokEventArgs<E extends TikTokStandardEvent> = E extends "Purchase"
36
- ? [data: TikTokEventData & { value: number; currency: CurrencyCode }]
37
- : [data?: TikTokEventData];
55
+ /**
56
+ * A standard event takes TikTok's parameters; `Purchase` needs a value and
57
+ * currency to be worth anything. A declared one takes what the project declared.
58
+ */
59
+ type TikTokEventArgs<E extends TikTokEventName> = E extends TikTokStandardEvent
60
+ ? E extends "Purchase"
61
+ ? [data: TikTokEventData & { value: number; currency: CurrencyCode }]
62
+ : [data?: TikTokEventData]
63
+ : EventDataArgs<TikTokCustomEvents[E & keyof TikTokCustomEvents]>;
38
64
 
39
65
  /** What `tiktok-pixel.ts` defines once marketing is granted. */
40
66
  interface TikTokWindow {
@@ -42,14 +68,15 @@ interface TikTokWindow {
42
68
  }
43
69
 
44
70
  /**
45
- * Records a standard event with TikTok's pixel — a lead sent, a booking made.
71
+ * Records an event with TikTok's pixel — a lead sent, a booking made. Declared
72
+ * events go through the same call.
46
73
  *
47
74
  * `ttq` exists only once the visitor granted marketing and the pixel loaded.
48
75
  * Before that, without consent, or where TikTok is not configured, the event is
49
76
  * dropped: no consent means nothing to send, and an analytics call must never
50
77
  * be the reason a page misbehaves.
51
78
  */
52
- export function tiktokEvent<E extends TikTokStandardEvent>(
79
+ export function tiktokEvent<E extends TikTokEventName>(
53
80
  event: E,
54
81
  ...[data]: TikTokEventArgs<E>
55
82
  ): void {
@@ -1,3 +1,21 @@
1
+ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
2
+
3
+ /**
4
+ * The project's Umami events, each with the data it carries, `undefined` for
5
+ * none. Umami has no standard events, so nothing compiles until a project
6
+ * declares its own, once:
7
+ *
8
+ * ```ts
9
+ * declare module "@escape-game-over/atlas/client" {
10
+ * interface UmamiEvents {
11
+ * "franchise-enquiry": { readonly country: string };
12
+ * }
13
+ * }
14
+ * ```
15
+ */
16
+ // biome-ignore lint/suspicious/noEmptyInterface: filled by each project's declaration.
17
+ export interface UmamiEvents {}
18
+
1
19
  /**
2
20
  * Records a named event with Umami — a form sent, a deck downloaded — from a
3
21
  * client script.
@@ -11,9 +29,10 @@
11
29
  * never who they are. For an event on a plain click, Umami's
12
30
  * `data-umami-event` attribute needs no script at all.
13
31
  */
14
- export function umamiEvent(
15
- name: string,
16
- data?: Readonly<Record<string, string>>
32
+ export function umamiEvent<E extends DeclaredEvent<UmamiEvents>>(
33
+ name: E,
34
+ // Flat string values; anything else in a declaration fails here.
35
+ ...[data]: EventDataArgs<UmamiEvents[E] & Readonly<Record<string, string>>>
17
36
  ): void {
18
37
  const umami = (
19
38
  window as unknown as {