@gradial/aci 0.1.28 → 0.1.30

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.
Files changed (73) hide show
  1. package/README.md +47 -0
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/dist/astro/index.d.ts +1 -0
  4. package/dist/astro/index.js +7 -0
  5. package/dist/collect/api.d.ts +24 -0
  6. package/dist/collect/api.js +63 -0
  7. package/dist/collect/batcher.d.ts +65 -0
  8. package/dist/collect/batcher.js +127 -0
  9. package/dist/collect/config.d.ts +30 -0
  10. package/dist/collect/config.js +53 -0
  11. package/dist/collect/consent/adapters/c15t.d.ts +2 -0
  12. package/dist/collect/consent/adapters/c15t.js +43 -0
  13. package/dist/collect/consent/adapters/cookiebot.d.ts +2 -0
  14. package/dist/collect/consent/adapters/cookiebot.js +48 -0
  15. package/dist/collect/consent/adapters/onetrust.d.ts +2 -0
  16. package/dist/collect/consent/adapters/onetrust.js +54 -0
  17. package/dist/collect/consent/adapters/trustarc.d.ts +2 -0
  18. package/dist/collect/consent/adapters/trustarc.js +65 -0
  19. package/dist/collect/consent/envelope.d.ts +49 -0
  20. package/dist/collect/consent/envelope.js +100 -0
  21. package/dist/collect/consent/mapping.d.ts +62 -0
  22. package/dist/collect/consent/mapping.js +129 -0
  23. package/dist/collect/declarative.d.ts +35 -0
  24. package/dist/collect/declarative.js +100 -0
  25. package/dist/collect/gapbuffer.d.ts +55 -0
  26. package/dist/collect/gapbuffer.js +134 -0
  27. package/dist/collect/index.d.ts +6 -0
  28. package/dist/collect/index.js +154 -0
  29. package/dist/collect/linkclicks.d.ts +12 -0
  30. package/dist/collect/linkclicks.js +35 -0
  31. package/dist/collect/pageviews.d.ts +22 -0
  32. package/dist/collect/pageviews.js +42 -0
  33. package/dist/collect/transport.d.ts +22 -0
  34. package/dist/collect/transport.js +52 -0
  35. package/dist/collect/ulid.d.ts +6 -0
  36. package/dist/collect/ulid.js +36 -0
  37. package/dist/collect/variationviews.d.ts +31 -0
  38. package/dist/collect/variationviews.js +80 -0
  39. package/dist/collect/webvitals.d.ts +38 -0
  40. package/dist/collect/webvitals.js +99 -0
  41. package/dist/content/index.d.ts +1 -0
  42. package/dist/content/index.js +1 -0
  43. package/dist/content/personalization.d.ts +57 -0
  44. package/dist/content/personalization.js +54 -0
  45. package/dist/content/types.d.ts +3 -0
  46. package/dist/next/collect-script.d.ts +12 -0
  47. package/dist/next/collect-script.js +15 -0
  48. package/dist/next/config.js +1 -0
  49. package/dist/next/index.d.ts +1 -0
  50. package/dist/next/index.js +1 -0
  51. package/dist/next/page.d.ts +2 -0
  52. package/dist/next/page.js +5 -2
  53. package/dist/providers/file.js +1 -8
  54. package/dist/react/VideoPlayer.d.ts +3 -3
  55. package/dist/react/index.d.ts +4 -4
  56. package/dist/react/index.js +1 -1
  57. package/dist/types/image.d.ts +27 -13
  58. package/dist/types/image.js +17 -17
  59. package/dist/types/media.d.ts +10 -4
  60. package/dist/types/video.d.ts +5 -2
  61. package/package.json +8 -9
  62. package/src/cli/validate-content.mjs +0 -0
  63. package/src/types/image.ts +32 -29
  64. package/templates/astro/template/src/fixtures/sections.ts +1 -1
  65. package/templates/astro/template/src/layouts/SiteLayout.astro +77 -1
  66. package/templates/astro/template/src/pages/[...slug].astro +3 -2
  67. package/templates/common/.content/config/site.json +5 -0
  68. package/templates/common/.content/schemas/site.schema.json +15 -0
  69. package/templates/common/README.md.tmpl +15 -0
  70. package/templates/nextjs/template/package.json.tmpl +2 -0
  71. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +132 -0
  72. package/templates/nextjs/template/src/app/layout.tsx +3 -1
  73. package/templates/nextjs/template/src/components/sections/HomeHero.tsx +1 -1
@@ -0,0 +1,134 @@
1
+ // Consent-gap buffer (design/05, REQ-8): holds events captured while the
2
+ // consent envelope grants personalization but the mint marker (aci_m) is
3
+ // absent — the window between consent and the first identity-bearing HTML
4
+ // response. Bounded at 50 (drop-oldest), persisted per-tab in
5
+ // sessionStorage["aci:buf"], purged on withdrawal, flushed through the
6
+ // normal batcher when the marker appears (checked at init and on every
7
+ // flush tick). Flushed events keep their eventIds.
8
+ export const GAP_BUFFER_KEY = "aci:buf";
9
+ export const GAP_BUFFER_CAP = 50;
10
+ const MINT_MARKER_COOKIE = "aci_m";
11
+ export class GapBuffer {
12
+ events;
13
+ storage;
14
+ debug;
15
+ constructor(env) {
16
+ this.debug = env?.debug;
17
+ this.storage = env?.storage;
18
+ this.events = this.restore();
19
+ }
20
+ push(event) {
21
+ this.events.push(event);
22
+ if (this.events.length > GAP_BUFFER_CAP) {
23
+ this.events.splice(0, this.events.length - GAP_BUFFER_CAP);
24
+ this.debug?.("buffer", { reason: "cap_drop_oldest" });
25
+ }
26
+ this.persist();
27
+ }
28
+ /** drain returns and clears all buffered events, preserving order and ids. */
29
+ drain() {
30
+ const drained = this.events;
31
+ this.events = [];
32
+ this.persist();
33
+ return drained;
34
+ }
35
+ /** purge empties the buffer (consent withdrawal). */
36
+ purge() {
37
+ if (this.events.length > 0) {
38
+ this.debug?.("buffer", { reason: "purged", count: this.events.length });
39
+ }
40
+ this.events = [];
41
+ this.persist();
42
+ }
43
+ size() {
44
+ return this.events.length;
45
+ }
46
+ restore() {
47
+ const storage = this.storage;
48
+ if (!storage)
49
+ return [];
50
+ let raw;
51
+ try {
52
+ raw = storage.getItem(GAP_BUFFER_KEY);
53
+ }
54
+ catch (err) {
55
+ // Privacy modes can deny storage entirely: degrade to memory-only.
56
+ this.storage = undefined;
57
+ this.debug?.("buffer", { reason: "storage_unavailable", error: String(err) });
58
+ return [];
59
+ }
60
+ if (raw == null)
61
+ return [];
62
+ try {
63
+ const parsed = JSON.parse(raw);
64
+ if (Array.isArray(parsed))
65
+ return parsed;
66
+ }
67
+ catch {
68
+ // Corrupt content: reset below.
69
+ }
70
+ try {
71
+ storage.removeItem(GAP_BUFFER_KEY);
72
+ }
73
+ catch {
74
+ this.storage = undefined;
75
+ }
76
+ return [];
77
+ }
78
+ persist() {
79
+ const storage = this.storage;
80
+ if (!storage)
81
+ return;
82
+ try {
83
+ if (this.events.length === 0) {
84
+ storage.removeItem(GAP_BUFFER_KEY);
85
+ }
86
+ else {
87
+ storage.setItem(GAP_BUFFER_KEY, JSON.stringify(this.events));
88
+ }
89
+ }
90
+ catch (err) {
91
+ this.storage = undefined;
92
+ this.debug?.("buffer", { reason: "storage_unavailable", error: String(err) });
93
+ }
94
+ }
95
+ }
96
+ /** hasMintMarker reports whether the edge-set aci_m marker cookie exists. */
97
+ export function hasMintMarker(doc) {
98
+ for (const part of doc.cookie.split(";")) {
99
+ const trimmed = part.trim();
100
+ if (trimmed.startsWith(MINT_MARKER_COOKIE + "=")) {
101
+ return trimmed.slice(MINT_MARKER_COOKIE.length + 1) !== "";
102
+ }
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * routeEvent implements the emit routing (design/05): events captured in
108
+ * the consent gap — envelope grants personalization AND the marker is
109
+ * absent — are buffered; everything else goes straight to the batcher (the
110
+ * collector enforces consent server-side).
111
+ */
112
+ export function routeEvent(event, deps) {
113
+ if (deps.grants() && !deps.marker()) {
114
+ deps.gapBuffer.push(event);
115
+ return;
116
+ }
117
+ deps.batcher.push(event);
118
+ }
119
+ /**
120
+ * maybeFlushGapBuffer drains the buffer through the normal batcher once the
121
+ * mint marker appears. Runs at init and on every batcher flush tick, so a
122
+ * mint by another tab or navigation is noticed.
123
+ */
124
+ export function maybeFlushGapBuffer(gapBuffer, batcher, marker, debug) {
125
+ if (gapBuffer.size() === 0)
126
+ return;
127
+ if (!marker())
128
+ return;
129
+ const events = gapBuffer.drain();
130
+ for (const event of events) {
131
+ batcher.push(event);
132
+ }
133
+ debug?.("buffer", { reason: "flushed", count: events.length });
134
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * init boots the SDK against a window. Idempotent (window.__aci_loaded);
3
+ * browsers missing the ~2019 baseline (sendBeacon, IntersectionObserver,
4
+ * crypto.getRandomValues) get a silent no-op.
5
+ */
6
+ export declare function init(win?: Window): void;
@@ -0,0 +1,154 @@
1
+ // Collect SDK entry point (design/05 init pseudocode). Bundled by esbuild
2
+ // into the single IIFE artifact served at /_aci/aci.js. Everything hangs off
3
+ // init(); every failure is swallowed after a debug record so the page is
4
+ // never affected (QR-2/3).
5
+ import { installCommandQueue } from "./api.js";
6
+ import { Batcher } from "./batcher.js";
7
+ import { createDebug, readTagConfig } from "./config.js";
8
+ import { start as startC15t } from "./consent/adapters/c15t.js";
9
+ import { start as startCookiebot } from "./consent/adapters/cookiebot.js";
10
+ import { start as startOneTrust } from "./consent/adapters/onetrust.js";
11
+ import { start as startTrustArc } from "./consent/adapters/trustarc.js";
12
+ import { applyConsent, envelopeGrantsPersonalization, readConsentCookie, sanitizePurposes, } from "./consent/envelope.js";
13
+ import { startDeclarative } from "./declarative.js";
14
+ import { GapBuffer, hasMintMarker, maybeFlushGapBuffer, routeEvent, } from "./gapbuffer.js";
15
+ import { startLinkClicks } from "./linkclicks.js";
16
+ import { startPageViews } from "./pageviews.js";
17
+ import { send } from "./transport.js";
18
+ import { ulid } from "./ulid.js";
19
+ import { startVariationViews } from "./variationviews.js";
20
+ import { startWebVitals } from "./webvitals.js";
21
+ const CMP_ADAPTERS = {
22
+ c15t: startC15t,
23
+ onetrust: startOneTrust,
24
+ cookiebot: startCookiebot,
25
+ trustarc: startTrustArc,
26
+ };
27
+ function hasBaseline(w) {
28
+ return (typeof w.navigator?.sendBeacon === "function" &&
29
+ typeof w.IntersectionObserver === "function" &&
30
+ typeof w.crypto?.getRandomValues === "function");
31
+ }
32
+ function safeSessionStorage(w) {
33
+ try {
34
+ return w.sessionStorage;
35
+ }
36
+ catch {
37
+ return undefined; // privacy modes can deny storage access entirely
38
+ }
39
+ }
40
+ /**
41
+ * init boots the SDK against a window. Idempotent (window.__aci_loaded);
42
+ * browsers missing the ~2019 baseline (sendBeacon, IntersectionObserver,
43
+ * crypto.getRandomValues) get a silent no-op.
44
+ */
45
+ export function init(win) {
46
+ const w = (win ??
47
+ (typeof window !== "undefined" ? window : undefined));
48
+ if (!w)
49
+ return;
50
+ if (w.__aci_loaded)
51
+ return;
52
+ w.__aci_loaded = true;
53
+ if (!hasBaseline(w))
54
+ return;
55
+ const cfg = readTagConfig(w.document.currentScript);
56
+ const debug = createDebug(cfg.debug);
57
+ try {
58
+ const doc = w.document;
59
+ const gapBuffer = new GapBuffer({ storage: safeSessionStorage(w), debug });
60
+ const grants = () => {
61
+ try {
62
+ return envelopeGrantsPersonalization(readConsentCookie(doc));
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ };
68
+ const marker = () => {
69
+ try {
70
+ return hasMintMarker(doc);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ };
76
+ const transportEnv = {
77
+ navigator: w.navigator,
78
+ fetch: typeof w.fetch === "function"
79
+ ? w.fetch.bind(w)
80
+ : undefined,
81
+ debug,
82
+ };
83
+ let batcher;
84
+ batcher = new Batcher({
85
+ send: (body) => send(body, transportEnv),
86
+ debug,
87
+ // The marker is re-checked on every flush tick, so a mint by another
88
+ // tab or navigation is noticed.
89
+ onTick: () => maybeFlushGapBuffer(gapBuffer, batcher, marker, debug),
90
+ timers: {
91
+ setInterval: (fn, ms) => w.setInterval(fn, ms),
92
+ clearInterval: (id) => w.clearInterval(id),
93
+ },
94
+ documentTarget: doc,
95
+ windowTarget: w,
96
+ });
97
+ const emit = (event) => {
98
+ routeEvent({
99
+ ...event,
100
+ eventId: ulid(undefined, (bytes) => w.crypto.getRandomValues(bytes)),
101
+ }, { grants, marker, gapBuffer, batcher });
102
+ };
103
+ installCommandQueue(w, cfg, {
104
+ track: (name, properties) => emit({ eventName: name, properties }),
105
+ consent: ({ p, src }) => {
106
+ const purposes = sanitizePurposes(p);
107
+ if (purposes === undefined) {
108
+ debug("consent", { reason: "malformed_purposes" });
109
+ return;
110
+ }
111
+ applyConsent(purposes, typeof src === "string" && src.trim() !== "" ? src.trim() : "manual", { document: doc, gapBuffer });
112
+ },
113
+ }, debug);
114
+ startPageViews(emit, w);
115
+ startDeclarative(emit, {
116
+ document: doc,
117
+ intersectionObserver: w.IntersectionObserver,
118
+ debug,
119
+ });
120
+ startLinkClicks(emit, { document: doc, debug });
121
+ startWebVitals(emit, {
122
+ performance: w.performance,
123
+ PerformanceObserver: w.PerformanceObserver,
124
+ window: w,
125
+ document: doc,
126
+ debug,
127
+ });
128
+ startVariationViews(emit, {
129
+ document: doc,
130
+ MutationObserver: w.MutationObserver,
131
+ debug,
132
+ });
133
+ if (cfg.preset !== "manual") {
134
+ CMP_ADAPTERS[cfg.preset]?.({
135
+ window: w,
136
+ document: doc,
137
+ categories: cfg.categories,
138
+ gapBuffer,
139
+ debug,
140
+ scheduler: {
141
+ setTimeout: (fn, ms) => w.setTimeout(fn, ms),
142
+ clearTimeout: (id) => w.clearTimeout(id),
143
+ },
144
+ });
145
+ }
146
+ batcher.start();
147
+ maybeFlushGapBuffer(gapBuffer, batcher, marker, debug);
148
+ debug("init", { config: cfg });
149
+ }
150
+ catch (err) {
151
+ debug("init", { error: String(err) });
152
+ }
153
+ }
154
+ init();
@@ -0,0 +1,12 @@
1
+ import type { PendingEvent } from "./batcher.js";
2
+ import type { DebugFn } from "./config.js";
3
+ export declare const LINK_CLICK_EVENT = "rum.link_click";
4
+ export interface LinkClickEnv {
5
+ document: Document;
6
+ debug?: DebugFn;
7
+ }
8
+ /**
9
+ * startLinkClicks installs one delegated click listener on document that
10
+ * emits rum.link_click for the closest a[href] ancestor of the click target.
11
+ */
12
+ export declare function startLinkClicks(emit: (event: PendingEvent) => void, env: LinkClickEnv): void;
@@ -0,0 +1,35 @@
1
+ // Link clicks (design/04): eventName "rum.link_click" with the current page
2
+ // url and { linkText, linkHref } properties. One delegated click listener on
3
+ // document; the closest a[href] ancestor of the click target is recorded.
4
+ // target="_blank" links are still captured — the click event is useful RUM
5
+ // even when the browser opens a new tab, and dropping them would lose a
6
+ // measurable fraction of outbound intent. Errors are swallowed silently.
7
+ export const LINK_CLICK_EVENT = "rum.link_click";
8
+ /**
9
+ * startLinkClicks installs one delegated click listener on document that
10
+ * emits rum.link_click for the closest a[href] ancestor of the click target.
11
+ */
12
+ export function startLinkClicks(emit, env) {
13
+ const { document: doc, debug } = env;
14
+ const loc = doc.defaultView?.location;
15
+ doc.addEventListener("click", (e) => {
16
+ try {
17
+ const target = e.target;
18
+ const el = target?.closest?.("a[href]");
19
+ if (!el)
20
+ return;
21
+ const anchor = el;
22
+ emit({
23
+ eventName: LINK_CLICK_EVENT,
24
+ url: loc?.href,
25
+ properties: {
26
+ linkText: anchor.textContent?.trim() ?? "",
27
+ linkHref: anchor.href,
28
+ },
29
+ });
30
+ }
31
+ catch (err) {
32
+ debug?.("capture", { reason: "link_click_failed", error: String(err) });
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,22 @@
1
+ import type { PendingEvent } from "./batcher.js";
2
+ export declare const PAGE_VIEW_EVENT = "rum.page_view";
3
+ interface HistoryLike {
4
+ pushState(...args: unknown[]): unknown;
5
+ replaceState(...args: unknown[]): unknown;
6
+ }
7
+ export interface PageViewWindow {
8
+ location: {
9
+ href: string;
10
+ };
11
+ document: {
12
+ referrer: string;
13
+ };
14
+ history: HistoryLike;
15
+ addEventListener(type: string, fn: () => void): void;
16
+ }
17
+ /**
18
+ * startPageViews emits the initial page view and hooks
19
+ * pushState/replaceState/popstate for SPA route changes.
20
+ */
21
+ export declare function startPageViews(emit: (event: PendingEvent) => void, win: PageViewWindow): void;
22
+ export {};
@@ -0,0 +1,42 @@
1
+ // Page views (design/04): eventName "rum.page_view" with url (the full href
2
+ // including query) and referrer — the SDK never populates utm, ingest
3
+ // derives it from the URL. One event per document load plus one per
4
+ // history-API route change; hash-only changes and same-URL repeats never
5
+ // double count.
6
+ export const PAGE_VIEW_EVENT = "rum.page_view";
7
+ function stripHash(href) {
8
+ const i = href.indexOf("#");
9
+ return i === -1 ? href : href.slice(0, i);
10
+ }
11
+ /**
12
+ * startPageViews emits the initial page view and hooks
13
+ * pushState/replaceState/popstate for SPA route changes.
14
+ */
15
+ export function startPageViews(emit, win) {
16
+ const view = () => emit({
17
+ eventName: PAGE_VIEW_EVENT,
18
+ url: win.location.href,
19
+ referrer: win.document.referrer,
20
+ });
21
+ let last = stripHash(win.location.href);
22
+ view();
23
+ const onNavigate = () => {
24
+ const current = stripHash(win.location.href);
25
+ if (current === last)
26
+ return; // hash-only change or same-URL repeat
27
+ last = current;
28
+ view();
29
+ };
30
+ const history = win.history;
31
+ const wrap = (method) => {
32
+ const original = history[method].bind(history);
33
+ history[method] = (...args) => {
34
+ const result = original(...args);
35
+ onNavigate();
36
+ return result;
37
+ };
38
+ };
39
+ wrap("pushState");
40
+ wrap("replaceState");
41
+ win.addEventListener("popstate", onNavigate);
42
+ }
@@ -0,0 +1,22 @@
1
+ import type { DebugFn } from "./config.js";
2
+ export declare const COLLECT_PATH = "/collect";
3
+ export interface BeaconNavigator {
4
+ sendBeacon?: (url: string, data: Blob) => boolean;
5
+ }
6
+ export type FetchLike = (input: string, init?: {
7
+ method?: string;
8
+ body?: Blob;
9
+ keepalive?: boolean;
10
+ }) => Promise<unknown>;
11
+ export interface TransportEnv {
12
+ /** Present key (even if undefined) overrides the global for tests. */
13
+ navigator?: BeaconNavigator | undefined;
14
+ fetch?: FetchLike | undefined;
15
+ debug?: DebugFn;
16
+ }
17
+ /**
18
+ * send posts one serialized batch envelope to the relative collect path.
19
+ * Every failure is swallowed after a structured debug record — the page is
20
+ * never affected.
21
+ */
22
+ export declare function send(body: string, env?: TransportEnv): void;
@@ -0,0 +1,52 @@
1
+ // Transport (design/05): navigator.sendBeacon("/collect", Blob text/plain)
2
+ // with a keepalive-fetch fallback. Fire-and-forget — no retry loop in v1;
3
+ // eventIds make a future retry layer safe. The path is RELATIVE so requests
4
+ // stay exactly same-origin (data-plane design/04).
5
+ export const COLLECT_PATH = "/collect";
6
+ /**
7
+ * send posts one serialized batch envelope to the relative collect path.
8
+ * Every failure is swallowed after a structured debug record — the page is
9
+ * never affected.
10
+ */
11
+ export function send(body, env) {
12
+ const debug = env?.debug;
13
+ const nav = env !== undefined && "navigator" in env
14
+ ? env.navigator
15
+ : globalThis.navigator;
16
+ const fetchFn = env !== undefined && "fetch" in env
17
+ ? env.fetch
18
+ : globalThis.fetch;
19
+ let blob;
20
+ try {
21
+ blob = new Blob([body], { type: "text/plain" });
22
+ }
23
+ catch {
24
+ debug?.("flush", { reason: "no_blob" });
25
+ return;
26
+ }
27
+ if (nav?.sendBeacon) {
28
+ try {
29
+ if (nav.sendBeacon(COLLECT_PATH, blob))
30
+ return;
31
+ }
32
+ catch {
33
+ // Fall through to the fetch fallback.
34
+ }
35
+ }
36
+ if (!fetchFn) {
37
+ debug?.("flush", { reason: "no_transport" });
38
+ return;
39
+ }
40
+ try {
41
+ void fetchFn(COLLECT_PATH, {
42
+ method: "POST",
43
+ body: blob,
44
+ keepalive: true,
45
+ }).catch((err) => {
46
+ debug?.("flush", { reason: "fetch_failed", error: String(err) });
47
+ });
48
+ }
49
+ catch (err) {
50
+ debug?.("flush", { reason: "fetch_failed", error: String(err) });
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ulid returns a 26-character uppercase Crockford-base32 ULID. The clock and
3
+ * the random source are injectable for deterministic tests; the defaults are
4
+ * Date.now() and crypto.getRandomValues.
5
+ */
6
+ export declare function ulid(now?: number, fillRandom?: (bytes: Uint8Array) => void): string;
@@ -0,0 +1,36 @@
1
+ // Client-side ULID for event ids (design/05): 26-char Crockford base32,
2
+ // 48-bit millisecond timestamp + 80 random bits from the platform CSPRNG.
3
+ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4
+ const TIME_CHARS = 10;
5
+ const RANDOM_BYTES = 10; // 80 bits -> 16 base32 chars
6
+ function defaultFillRandom(bytes) {
7
+ globalThis.crypto.getRandomValues(bytes);
8
+ }
9
+ /**
10
+ * ulid returns a 26-character uppercase Crockford-base32 ULID. The clock and
11
+ * the random source are injectable for deterministic tests; the defaults are
12
+ * Date.now() and crypto.getRandomValues.
13
+ */
14
+ export function ulid(now, fillRandom) {
15
+ let ms = now ?? Date.now();
16
+ let time = "";
17
+ for (let i = 0; i < TIME_CHARS; i++) {
18
+ time = ALPHABET[ms % 32] + time;
19
+ ms = Math.floor(ms / 32);
20
+ }
21
+ const bytes = new Uint8Array(RANDOM_BYTES);
22
+ (fillRandom ?? defaultFillRandom)(bytes);
23
+ // Pack 80 bits into 16 5-bit groups, most significant bit first.
24
+ let random = "";
25
+ let bitBuffer = 0;
26
+ let bitCount = 0;
27
+ for (const byte of bytes) {
28
+ bitBuffer = (bitBuffer << 8) | byte;
29
+ bitCount += 8;
30
+ while (bitCount >= 5) {
31
+ random += ALPHABET[(bitBuffer >>> (bitCount - 5)) & 31];
32
+ bitCount -= 5;
33
+ }
34
+ }
35
+ return time + random;
36
+ }
@@ -0,0 +1,31 @@
1
+ import type { PendingEvent } from "./batcher.js";
2
+ import type { DebugFn } from "./config.js";
3
+ export declare const VARIATION_VIEW_EVENT = "rum.variation_view";
4
+ interface NodeLike {
5
+ nodeType: number;
6
+ element?: Element;
7
+ }
8
+ interface MutationRecordLike {
9
+ addedNodes: ArrayLike<NodeLike>;
10
+ }
11
+ interface MutationObserverLike {
12
+ observe(target: Node, options: {
13
+ subtree?: boolean;
14
+ childList?: boolean;
15
+ }): void;
16
+ disconnect(): void;
17
+ }
18
+ type MutationCallback = (records: MutationRecordLike[], observer: MutationObserverLike) => void;
19
+ type MutationObserverCtor = new (callback: MutationCallback) => MutationObserverLike;
20
+ export interface VariationViewEnv {
21
+ document: Document;
22
+ MutationObserver?: MutationObserverCtor | undefined;
23
+ debug?: DebugFn;
24
+ }
25
+ /**
26
+ * startVariationViews observes document.body for elements tagged with
27
+ * data-aci-variation and emits rum.variation_view once per element per page
28
+ * view. Returns silently when MutationObserver is unavailable.
29
+ */
30
+ export declare function startVariationViews(emit: (event: PendingEvent) => void, env: VariationViewEnv): void;
31
+ export {};
@@ -0,0 +1,80 @@
1
+ // Variation views (design/04): eventName "rum.variation_view" with
2
+ // { variantKey }. A MutationObserver on document.body watches for elements
3
+ // tagged with data-aci-variation (value = variation/experience key) and emits
4
+ // once per element per page view (WeakSet, same shape as declarative visible
5
+ // tracking). If MutationObserver is missing the collector is a no-op. Errors
6
+ // are swallowed silently.
7
+ export const VARIATION_VIEW_EVENT = "rum.variation_view";
8
+ const VARIATION_ATTR = "data-aci-variation";
9
+ /**
10
+ * startVariationViews observes document.body for elements tagged with
11
+ * data-aci-variation and emits rum.variation_view once per element per page
12
+ * view. Returns silently when MutationObserver is unavailable.
13
+ */
14
+ export function startVariationViews(emit, env) {
15
+ const { document: doc, debug } = env;
16
+ const Observer = env.MutationObserver;
17
+ if (typeof Observer !== "function")
18
+ return;
19
+ const loc = doc.defaultView?.location;
20
+ const seen = new WeakSet();
21
+ const emitFor = (el) => {
22
+ const key = el.getAttribute(VARIATION_ATTR);
23
+ if (key == null)
24
+ return;
25
+ if (seen.has(el))
26
+ return;
27
+ seen.add(el);
28
+ emit({
29
+ eventName: VARIATION_VIEW_EVENT,
30
+ url: loc?.href,
31
+ properties: { variantKey: key },
32
+ });
33
+ };
34
+ const scan = (root) => {
35
+ try {
36
+ if (root.hasAttribute(VARIATION_ATTR))
37
+ emitFor(root);
38
+ for (const el of Array.from(root.querySelectorAll(`[${VARIATION_ATTR}]`))) {
39
+ emitFor(el);
40
+ }
41
+ }
42
+ catch (err) {
43
+ debug?.("capture", { reason: "variation_scan_failed", error: String(err) });
44
+ }
45
+ };
46
+ try {
47
+ const body = doc.body ?? doc.documentElement;
48
+ if (body)
49
+ scan(body);
50
+ }
51
+ catch (err) {
52
+ debug?.("capture", { reason: "variation_initial_scan_failed", error: String(err) });
53
+ }
54
+ try {
55
+ const body = doc.body ?? doc.documentElement;
56
+ if (!body)
57
+ return;
58
+ const observer = new Observer((records) => {
59
+ try {
60
+ for (const record of records) {
61
+ for (let i = 0; i < record.addedNodes.length; i++) {
62
+ const node = record.addedNodes[i];
63
+ if (node?.nodeType !== 1)
64
+ continue;
65
+ const el = node.element;
66
+ if (el)
67
+ scan(el);
68
+ }
69
+ }
70
+ }
71
+ catch (err) {
72
+ debug?.("capture", { reason: "variation_observer_failed", error: String(err) });
73
+ }
74
+ });
75
+ observer.observe(body, { subtree: true, childList: true });
76
+ }
77
+ catch (err) {
78
+ debug?.("capture", { reason: "variation_observe_failed", error: String(err) });
79
+ }
80
+ }
@@ -0,0 +1,38 @@
1
+ import type { PendingEvent } from "./batcher.js";
2
+ import type { DebugFn } from "./config.js";
3
+ export declare const WEB_VITAL_EVENT = "rum.web_vital";
4
+ interface PerformanceEntryLike {
5
+ entryType: string;
6
+ name?: string;
7
+ startTime: number;
8
+ value?: number;
9
+ responseStart?: number;
10
+ requestStart?: number;
11
+ hadRecentInput?: boolean;
12
+ }
13
+ interface PerformanceLike {
14
+ getEntriesByType(type: string): PerformanceEntryLike[];
15
+ }
16
+ interface WindowLike {
17
+ location: {
18
+ href: string;
19
+ };
20
+ addEventListener(type: string, fn: () => void): void;
21
+ }
22
+ interface DocumentLike {
23
+ visibilityState?: string;
24
+ addEventListener(type: string, fn: () => void): void;
25
+ }
26
+ export interface WebVitalsEnv {
27
+ performance?: PerformanceLike;
28
+ PerformanceObserver?: typeof PerformanceObserver | undefined;
29
+ window?: WindowLike | undefined;
30
+ document?: DocumentLike | undefined;
31
+ debug?: DebugFn;
32
+ }
33
+ /**
34
+ * startWebVitals observes Core Web Vitals and emits one rum.web_vital per
35
+ * vital. Returns silently when PerformanceObserver is unavailable.
36
+ */
37
+ export declare function startWebVitals(emit: (event: PendingEvent) => void, env: WebVitalsEnv): void;
38
+ export {};