@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 +1 -1
- package/src/analytics/axon-pixel.ts +6 -1
- package/src/analytics/clarity.ts +29 -0
- package/src/analytics/drip.ts +5 -1
- package/src/analytics/index.ts +8 -0
- package/src/analytics/meta-pixel.ts +7 -1
- package/src/analytics/snap-pixel.ts +6 -1
- package/src/analytics/tiktok-pixel.ts +7 -1
- package/src/astro/Clarity.astro +18 -0
- package/src/astro/ConsentBanner.astro +3 -1
- package/src/astro/GoogleMap.astro +5 -0
- package/src/astro/clarity-event.ts +15 -0
- package/src/astro/clarity.ts +60 -0
- package/src/astro/client.ts +1 -0
- package/src/astro/consent-gate.ts +21 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { warn } from "../warn.ts";
|
|
2
2
|
import { UUID } from "./snap-pixel.ts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
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
|
+
}
|
package/src/analytics/drip.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { warn } from "../warn.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
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;
|
package/src/analytics/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
+
});
|
package/src/astro/client.ts
CHANGED
|
@@ -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
|
+
}
|