@escape-game-over/atlas 0.1.59 → 0.1.60

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.59",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -1,7 +1,12 @@
1
1
  import { warn } from "../warn.ts";
2
2
  import { UUID } from "./snap-pixel.ts";
3
3
 
4
- /** AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that. */
4
+ /**
5
+ * AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that.
6
+ *
7
+ * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/axon-pixel-native-js
8
+ * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/events-and-objects
9
+ */
5
10
  export interface AxonPixelSettings {
6
11
  /** From the Axon dashboard: a UUID. */
7
12
  readonly eventKey: string;
@@ -0,0 +1,29 @@
1
+ import { warn } from "../warn.ts";
2
+
3
+ /**
4
+ * Microsoft Clarity. It has a cookieless mode, so like Google it loads for
5
+ * every visitor: without consent it records each page view on its own and sets
6
+ * no cookie, and the visitor's answer switches its cookies on or off.
7
+ *
8
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup
9
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/consent-mode
10
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-consent-api-v2
11
+ */
12
+ export interface ClaritySettings {
13
+ /** From the Clarity project's settings: 10 lowercase letters and digits. */
14
+ readonly projectId: string;
15
+ }
16
+
17
+ /** Warns about an id that is not a project id: it would report nowhere. */
18
+ export function checkClarity(
19
+ clarity: ClaritySettings | undefined,
20
+ at: string
21
+ ): void {
22
+ if (clarity === undefined || /^[a-z0-9]{10}$/.test(clarity.projectId)) {
23
+ return;
24
+ }
25
+ warn(
26
+ at,
27
+ `Clarity project id "${clarity.projectId}" is not 10 lowercase letters and digits, so it reports nowhere. Copy it from the Clarity project's settings.`
28
+ );
29
+ }
@@ -1,6 +1,10 @@
1
1
  import { warn } from "../warn.ts";
2
2
 
3
- /** Drip's site tracking. Marketing: it loads only once a visitor grants that. */
3
+ /**
4
+ * Drip's site tracking. Marketing: it loads only once a visitor grants that.
5
+ *
6
+ * @see https://help.drip.com/hc/en-us/articles/4424702539789-Install-Your-JavaScript-Snippet
7
+ */
4
8
  export interface DripSettings {
5
9
  /** From Drip's Site Setup: digits, the `_dcs.account` of its snippet. */
6
10
  readonly accountId: string;
@@ -1,6 +1,7 @@
1
1
  import type { MetaTag } from "../meta/tag.ts";
2
2
  import type { HttpsUrl } from "../url.ts";
3
3
  import { type AxonPixelSettings, checkAxonPixel } from "./axon-pixel.ts";
4
+ import { type ClaritySettings, checkClarity } from "./clarity.ts";
4
5
  import { checkDrip, type DripSettings } from "./drip.ts";
5
6
  import { type GoogleSettings, googleEmits, googleScripts } from "./google.ts";
6
7
  import { checkMetaPixel, type MetaPixelSettings } from "./meta-pixel.ts";
@@ -29,6 +30,7 @@ import {
29
30
  */
30
31
 
31
32
  export type { AxonPixelSettings } from "./axon-pixel.ts";
33
+ export type { ClaritySettings } from "./clarity.ts";
32
34
  export type { DripSettings } from "./drip.ts";
33
35
  export {
34
36
  CONSENT_UPDATE_GLOBAL,
@@ -60,6 +62,8 @@ export interface AnalyticsSettings {
60
62
  readonly axon?: AxonPixelSettings;
61
63
  /** Loaded by `ConsentBanner`, after marketing is granted. */
62
64
  readonly drip?: DripSettings;
65
+ /** Loaded by `ConsentBanner` for every visitor, cookieless until analytics is granted. */
66
+ readonly clarity?: ClaritySettings;
63
67
  }
64
68
 
65
69
  /**
@@ -85,6 +89,7 @@ export function analyticsScripts(
85
89
  checkSnapPixel(analytics.snapchat, origin);
86
90
  checkAxonPixel(analytics.axon, origin);
87
91
  checkDrip(analytics.drip, origin);
92
+ checkClarity(analytics.clarity, origin);
88
93
  const google = googleScripts(analytics.google, origin);
89
94
  return {
90
95
  head: [...google.head, ...umamiScripts(analytics.umami)],
@@ -133,6 +138,9 @@ export function consentVendors(
133
138
  axon: analytics.axon !== undefined,
134
139
  // No consent mode at all.
135
140
  drip: analytics.drip !== undefined,
141
+ // Cookieless without consent, but the answer is what switches its
142
+ // cookies on, so it needs the banner.
143
+ clarity: analytics.clarity !== undefined,
136
144
  };
137
145
 
138
146
  return (Object.keys(VENDORS) as (keyof AnalyticsSettings)[]).filter(
@@ -1,7 +1,13 @@
1
1
  import type { NonEmpty } from "../types.ts";
2
2
  import { warn } from "../warn.ts";
3
3
 
4
- /** Meta's pixel. Marketing: it loads only once a visitor grants that. */
4
+ /**
5
+ * Meta's pixel. Marketing: it loads only once a visitor grants that.
6
+ *
7
+ * @see https://developers.facebook.com/docs/meta-pixel/get-started
8
+ * @see https://developers.facebook.com/docs/meta-pixel/reference
9
+ * @see https://developers.facebook.com/docs/meta-pixel/implementation/gdpr
10
+ */
5
11
  export interface MetaPixelSettings {
6
12
  /** From Events Manager: 15 or 16 digits. Several share one script. */
7
13
  readonly pixelIds: NonEmpty<string>;
@@ -1,7 +1,12 @@
1
1
  import type { NonEmpty } from "../types.ts";
2
2
  import { warn } from "../warn.ts";
3
3
 
4
- /** Snapchat's pixel. Marketing: it loads only once a visitor grants that. */
4
+ /**
5
+ * Snapchat's pixel. Marketing: it loads only once a visitor grants that.
6
+ *
7
+ * @see https://businesshelp.snapchat.com/s/article/pixel-direct-implementation
8
+ * @see https://forbusiness.snapchat.com/en-US/blog/the-snap-pixel-how-it-works-and-how-to-install-it
9
+ */
5
10
  export interface SnapPixelSettings {
6
11
  /** From Snap's Events Manager: a UUID. */
7
12
  readonly pixelIds: NonEmpty<string>;
@@ -1,7 +1,13 @@
1
1
  import type { NonEmpty } from "../types.ts";
2
2
  import { warn } from "../warn.ts";
3
3
 
4
- /** TikTok's pixel. Marketing: it loads only once a visitor grants that. */
4
+ /**
5
+ * TikTok's pixel. Marketing: it loads only once a visitor grants that.
6
+ *
7
+ * @see https://ads.tiktok.com/help/article/get-started-pixel
8
+ * @see https://ads.tiktok.com/help/article/standard-events-parameters
9
+ * @see https://ads.tiktok.com/help/article/how-to-use-cookies-with-tiktok-pixel
10
+ */
5
11
  export interface TikTokPixelSettings {
6
12
  /** From TikTok Events Manager: 20 capital letters and digits. */
7
13
  readonly pixelIds: NonEmpty<string>;
@@ -0,0 +1,18 @@
1
+ ---
2
+ /** Internal to `ConsentBanner.astro`, so Clarity's script ships only where it is configured. */
3
+ import type { ClaritySettings } from "../analytics/clarity.ts";
4
+ import AtlasElement from "./AtlasElement.astro";
5
+ import { clarityElement } from "./clarity.ts";
6
+
7
+ interface Props {
8
+ readonly settings: ClaritySettings;
9
+ }
10
+ ---
11
+
12
+ <script src="./clarity.ts" />
13
+
14
+ <AtlasElement
15
+ of={clarityElement}
16
+ data-project-id={Astro.props.settings.projectId}
17
+ hidden
18
+ />
@@ -5,12 +5,13 @@
5
5
  * buttons.
6
6
  *
7
7
  * Renders nothing — and ships no script — when this site's analytics need no
8
- * permission. Also loads the vendors that wait for consent, such as the Meta, TikTok, Snap and Axon pixels and Drip. Otherwise the banner starts hidden and shows only to a visitor
8
+ * permission. Also loads the vendors that wait for consent, such as the Meta, TikTok, Snap and Axon pixels, Drip and Clarity. Otherwise the banner starts hidden and shows only to a visitor
9
9
  * with no answer on record, or when a `data-consent-reopen` control is pressed.
10
10
  */
11
11
  import type { HTMLAttributes } from "astro/types";
12
12
  import { type AnalyticsSettings, consentRequired } from "../analytics/index.ts";
13
13
  import AxonPixel from "./AxonPixel.astro";
14
+ import Clarity from "./Clarity.astro";
14
15
  import ConsentElement from "./ConsentElement.astro";
15
16
  import Drip from "./Drip.astro";
16
17
  import MetaPixel from "./MetaPixel.astro";
@@ -34,5 +35,6 @@ const { analytics, ...attrs } = Astro.props;
34
35
  {analytics?.snapchat && <SnapPixel settings={analytics.snapchat} />}
35
36
  {analytics?.axon && <AxonPixel settings={analytics.axon} />}
36
37
  {analytics?.drip && <Drip settings={analytics.drip} />}
38
+ {analytics?.clarity && <Clarity settings={analytics.clarity} />}
37
39
  </>
38
40
  )}
@@ -11,6 +11,11 @@
11
11
  *
12
12
  * Give the element its size; the map fills it.
13
13
  *
14
+ * @see https://developers.google.com/maps/documentation/javascript/load-maps-js-api
15
+ * @see https://developers.google.com/maps/documentation/javascript/advanced-markers/overview
16
+ * @see https://developers.google.com/maps/documentation/get-map-id
17
+ * @see https://developers.google.com/maps/billing-and-pricing/pricing
18
+ *
14
19
  * ```astro
15
20
  * <GoogleMap apiKey={key} mapId={id} colorScheme="DARK" places={[{ at, label, href }]} class="block h-96">
16
21
  * <button type="button">Show map</button>
@@ -0,0 +1,15 @@
1
+ /** What `clarity.ts` defines once the page has loaded. */
2
+ interface ClarityWindow {
3
+ clarity?: (...args: unknown[]) => void;
4
+ }
5
+
6
+ /**
7
+ * Marks the current Clarity recording with a named event — a form sent, a
8
+ * booking opened — so recordings can be filtered by it.
9
+ *
10
+ * Sent with or without consent: Clarity records either way, and without it
11
+ * sets no cookie. Dropped before Clarity loads, or where it is not configured.
12
+ */
13
+ export function clarityEvent(name: string): void {
14
+ (window as unknown as ClarityWindow).clarity?.("event", name);
15
+ }
@@ -0,0 +1,60 @@
1
+ import { appendScript } from "./append-script.ts";
2
+ import type { ConsentChoices } from "./consent.ts";
3
+ import { withConsent } from "./consent-gate.ts";
4
+ import { element } from "./element.ts";
5
+ import { data } from "./ref.ts";
6
+
7
+ /** Clarity's `clarity`: a queue until its script arrives and replays it. */
8
+ interface Clarity {
9
+ (...args: unknown[]): void;
10
+ q?: unknown[];
11
+ }
12
+
13
+ interface ClarityWindow {
14
+ clarity?: Clarity;
15
+ }
16
+
17
+ /** Clarity's own base code, as it defines `clarity` before the script loads. */
18
+ function defineClarity(): Clarity {
19
+ const global = window as unknown as ClarityWindow;
20
+ if (global.clarity !== undefined) return global.clarity;
21
+ // `arguments` is the list of whatever a call passed, which every regular
22
+ // `function` gets without declaring it (an arrow function does not). The
23
+ // queue stores that object rather than an array because Clarity's official
24
+ // snippet does: it is the shape Clarity promises its script will replay.
25
+ const clarity = function () {
26
+ // biome-ignore lint/complexity/noArguments: see above.
27
+ const args = arguments;
28
+ clarity.q ??= [];
29
+ clarity.q.push(args);
30
+ } as Clarity;
31
+ global.clarity = clarity;
32
+ return clarity;
33
+ }
34
+
35
+ /** Clarity's consent call, in its own spelling. */
36
+ const consentv2 = (choices: ConsentChoices) => ({
37
+ ad_Storage: choices.marketing,
38
+ analytics_Storage: choices.analytics,
39
+ });
40
+
41
+ /**
42
+ * Loads Clarity with the answer so far, then passes on every answer. Stated
43
+ * even when denied: a Clarity project set to use cookies by default would
44
+ * otherwise set them before anyone was asked.
45
+ */
46
+ export function clarity(projectId: string, signal?: AbortSignal): void {
47
+ let loaded = false;
48
+ withConsent((choices) => {
49
+ const queue = defineClarity();
50
+ queue("consentv2", consentv2(choices));
51
+ if (loaded) return;
52
+ loaded = true;
53
+ appendScript(`https://www.clarity.ms/tag/${projectId}`);
54
+ }, signal);
55
+ }
56
+
57
+ /** Internal to `Clarity.astro`. */
58
+ export const clarityElement = element("atlas-clarity", ({ root, signal }) => {
59
+ clarity(data(root, "project-id"), signal);
60
+ });
@@ -13,6 +13,7 @@ export * from "./attribution.ts";
13
13
  export * from "./axon-event.ts";
14
14
  export * from "./background-video.ts";
15
15
  export * from "./carousel.ts";
16
+ export * from "./clarity-event.ts";
16
17
  // Reading only: a form that reports what the visitor agreed to. Recording an
17
18
  // answer stays with the banner.
18
19
  export { type ConsentRecord, readConsent } from "./consent.ts";
@@ -46,3 +46,24 @@ export function whenGranted(
46
46
  { signal }
47
47
  );
48
48
  }
49
+
50
+ /**
51
+ * For a vendor with a cookieless mode, which loads whatever the answer: after
52
+ * `load`, `apply` gets the answer so far (everything denied until one is
53
+ * given), then every answer given on this page.
54
+ */
55
+ export function withConsent(
56
+ apply: (choices: ConsentChoices) => void,
57
+ signal?: AbortSignal
58
+ ): void {
59
+ const start = () => {
60
+ apply(readConsent() ?? { analytics: "denied", marketing: "denied" });
61
+ addEventListener(
62
+ CONSENT_EVENT,
63
+ (event) => apply((event as CustomEvent<ConsentChoices>).detail),
64
+ { signal }
65
+ );
66
+ };
67
+ if (document.readyState === "complete") start();
68
+ else addEventListener("load", start, { once: true, signal });
69
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export {
23
23
  type AnalyticsSettings,
24
24
  type AnalyticsTags,
25
25
  type AxonPixelSettings,
26
+ type ClaritySettings,
26
27
  CONSENT_UPDATE_GLOBAL,
27
28
  type ConsentDefaults,
28
29
  type ConsentState,