@escape-game-over/atlas 0.1.65 → 0.1.67
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/docs/checks.md +8 -1
- package/docs/client-scripts.md +82 -0
- package/package.json +1 -1
- package/src/analytics/google-tag.ts +24 -1
- package/src/analytics/google.ts +22 -0
- package/src/astro/clarity-event.ts +19 -1
- package/src/astro/client.ts +1 -0
- package/src/astro/drip-event.ts +24 -4
- package/src/astro/event-data.ts +15 -0
- package/src/astro/google-event.ts +119 -0
- package/src/astro/meta-event.ts +57 -26
- package/src/astro/tiktok-event.ts +33 -6
- package/src/astro/umami-event.ts +22 -3
package/docs/checks.md
CHANGED
|
@@ -89,6 +89,13 @@ augmentation is real. Both examples carry one:
|
|
|
89
89
|
two files in it, and [`b2b`](../examples/b2b/type-tests/wiring.ts) against one
|
|
90
90
|
with none, where the union is empty and *every* path is rejected.
|
|
91
91
|
|
|
92
|
+
Declared analytics events are the other augmentation, and the opposite case:
|
|
93
|
+
nothing is inferred, so a declaration means the same thing here as in a project.
|
|
94
|
+
`type-tests/declared-events.ts` writes one by the package's own name, as a
|
|
95
|
+
project does, and asserts that an undeclared name, missing required data and
|
|
96
|
+
data on an event declared without any all fail. The declaration holds for the
|
|
97
|
+
whole program, so the runtime tests call the names it declares.
|
|
98
|
+
|
|
92
99
|
## Runtime tests
|
|
93
100
|
|
|
94
101
|
```bash
|
|
@@ -117,7 +124,7 @@ dependencies at all. Vitest type-checks them itself when it runs them.
|
|
|
117
124
|
| `breadcrumbs.test.ts` | the trail, a dropped ancestor, `orphanSegments`, and how a gap is reported |
|
|
118
125
|
| `jsonld/*.test.ts` | that a `</script>` in any value cannot close the block, each node's shape and `@id`, and how a price table becomes offers |
|
|
119
126
|
| `analytics.test.ts` | Umami's attributes, its three-state booleans, the `domains` list that would record nothing, and that `consentRequired` answers exactly when a tag was emitted |
|
|
120
|
-
| `google-analytics.test.ts` | that consent is denied first and precedes every tag, one loader for many ids, dead `UA-` properties,
|
|
127
|
+
| `google-analytics.test.ts` | that consent is denied first and precedes every tag, one loader for many ids, dead `UA-` properties, the preconnect — once, ahead of the block that writes the loader's URL, never `crossorigin` — and where `googleEvent` sends, per setting |
|
|
121
128
|
| `contact.test.ts` | the E.164 a `tel:` needs — trunk zero dropped, spacing stripped — and the displayed form kept |
|
|
122
129
|
| `hours.test.ts` | collapsing a week into runs, week start changing the answer, and every impossible week that throws |
|
|
123
130
|
| `money.test.ts` | a bare count widened to a band, the span of a table, and the gaps and overlaps that throw |
|
package/docs/client-scripts.md
CHANGED
|
@@ -535,6 +535,88 @@ move before this pauses them. Left in, everything still works. `muted` is also
|
|
|
535
535
|
set on attach, since no browser will start an unmuted video nobody pressed play
|
|
536
536
|
on.
|
|
537
537
|
|
|
538
|
+
## Events
|
|
539
|
+
|
|
540
|
+
One function per vendor, all dropped silently where the vendor is not
|
|
541
|
+
configured, not loaded or not permitted — an analytics call must never be the
|
|
542
|
+
reason a page misbehaves.
|
|
543
|
+
|
|
544
|
+
| Function | Names | Consent |
|
|
545
|
+
| -------------- | ------------------------------ | ------------------------------------------ |
|
|
546
|
+
| `googleEvent` | GA4's recommended, or declared | Consent Mode, which Google already applies |
|
|
547
|
+
| `metaEvent` | Meta's standard, or declared | the pixel loads only once granted |
|
|
548
|
+
| `tiktokEvent` | TikTok's standard, or declared | the pixel loads only once granted |
|
|
549
|
+
| `snapEvent` | Snap's standard only | checked on every call |
|
|
550
|
+
| `axonEvent` | Axon's standard only | checked on every call |
|
|
551
|
+
| `umamiEvent` | declared only | none needed: Umami sets no cookie |
|
|
552
|
+
| `dripEvent` | declared only | checked on every call |
|
|
553
|
+
| `clarityEvent` | declared only | none needed: cookieless until granted |
|
|
554
|
+
|
|
555
|
+
**A project declares its own events once**, for every site it builds, by
|
|
556
|
+
merging into the interface each function reads — the arrangement
|
|
557
|
+
`PublicFileRegistry` uses. Each name maps to the data it carries, `undefined` for
|
|
558
|
+
none:
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
// src/events.ts — any file the project's tsconfig includes
|
|
562
|
+
export {};
|
|
563
|
+
|
|
564
|
+
declare module "@escape-game-over/atlas/client" {
|
|
565
|
+
interface GoogleCustomEvents {
|
|
566
|
+
form_submission_contact: undefined;
|
|
567
|
+
form_submission_quote: { readonly room: string };
|
|
568
|
+
}
|
|
569
|
+
interface UmamiEvents {
|
|
570
|
+
"franchise-enquiry": { readonly country: string };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
googleEvent("form_submission_contact");
|
|
577
|
+
googleEvent("form_submission_quote", { room: "Tomb" });
|
|
578
|
+
googleEvent("form_submissions_contact"); // does not compile
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
A name nobody declared does not compile, so a typo cannot quietly become a
|
|
582
|
+
second event in a report — which is why this is not a `string` with the standard
|
|
583
|
+
names offered for autocomplete. Data declared with a required field is required
|
|
584
|
+
at every call. The interfaces are `GoogleCustomEvents`, `MetaCustomEvents`,
|
|
585
|
+
`TikTokCustomEvents`, `UmamiEvents`, `DripEvents` and `ClarityEvents`; Clarity
|
|
586
|
+
carries a name only, so its entries are all `undefined`. Snap and Axon take no
|
|
587
|
+
declarations because they accept no names but their own.
|
|
588
|
+
|
|
589
|
+
**The file must be a module** — the `export {}` above. Without an import or an
|
|
590
|
+
export, `declare module` *replaces* the package's types instead of adding to
|
|
591
|
+
them, and every import from it breaks.
|
|
592
|
+
|
|
593
|
+
**Declared data is sent as written, so it names what the visitor chose, never
|
|
594
|
+
who they are.** Google's terms forbid sending it anything that identifies a
|
|
595
|
+
person — a name, an email, a phone number, a free-text message — and its
|
|
596
|
+
[guidance](https://support.google.com/analytics/answer/6366371) names event
|
|
597
|
+
parameters as the usual leak. The standard parameter types have no field for
|
|
598
|
+
any of those; a declaration is where one would have to be added, deliberately.
|
|
599
|
+
|
|
600
|
+
### `googleEvent`: where it goes
|
|
601
|
+
|
|
602
|
+
The deployment's `google` settings decide, not the caller. The head script
|
|
603
|
+
defines a global — `GOOGLE_EVENT_GLOBAL`, beside `CONSENT_UPDATE_GLOBAL` and for
|
|
604
|
+
the same reason — that sends:
|
|
605
|
+
|
|
606
|
+
- with `tagIds`: `gtag("event", name, data)`, which `gtag.js` reads;
|
|
607
|
+
- with `containerIds`: `dataLayer.push({ event: name, ...data })`, which is what
|
|
608
|
+
a Tag Manager *Custom Event* trigger listens for — the `dataLayer.push` a
|
|
609
|
+
marketing agency's brief asks for;
|
|
610
|
+
- with both, both. A container that also runs a GA4 tag on the same event then
|
|
611
|
+
counts it twice; that is set up in Tag Manager, where lib cannot see it.
|
|
612
|
+
|
|
613
|
+
The ecommerce events — `purchase`, `add_to_cart`, `view_item` and the rest — go
|
|
614
|
+
to Tag Manager under an `ecommerce` key, after a `{ ecommerce: null }` that
|
|
615
|
+
clears the previous one, which is the shape its GA4 tags read items and revenue
|
|
616
|
+
from. `purchase` does not compile without `value`, `currency` and
|
|
617
|
+
`transaction_id`: without them GA records a sale of nothing, and a reload
|
|
618
|
+
records it again.
|
|
619
|
+
|
|
538
620
|
## Testing
|
|
539
621
|
|
|
540
622
|
None of these tests has a document, and none needs one: every module under test
|
package/package.json
CHANGED
|
@@ -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);
|
package/src/analytics/google.ts
CHANGED
|
@@ -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",
|
|
@@ -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:
|
|
31
|
+
export function clarityEvent(name: DeclaredEvent<ClarityEvents>): void {
|
|
14
32
|
(window as unknown as ClarityWindow).clarity?.("event", name);
|
|
15
33
|
}
|
package/src/astro/client.ts
CHANGED
|
@@ -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";
|
package/src/astro/drip-event.ts
CHANGED
|
@@ -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.
|
|
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:
|
|
18
|
-
|
|
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
|
+
}
|
package/src/astro/meta-event.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
79
|
+
export function tiktokEvent<E extends TikTokEventName>(
|
|
53
80
|
event: E,
|
|
54
81
|
...[data]: TikTokEventArgs<E>
|
|
55
82
|
): void {
|
package/src/astro/umami-event.ts
CHANGED
|
@@ -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:
|
|
16
|
-
|
|
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 {
|