@mrrocket/sdk 0.0.0-stage → 0.2.0
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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/api.d.ts +113 -0
- package/dist/api.js +71 -0
- package/dist/api.js.map +1 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +25 -0
- package/dist/browser.js.map +1 -0
- package/dist/capture.d.ts +50 -0
- package/dist/capture.js +91 -0
- package/dist/capture.js.map +1 -0
- package/dist/client.d.ts +83 -0
- package/dist/client.js +468 -0
- package/dist/client.js.map +1 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +15 -0
- package/dist/context.js.map +1 -0
- package/dist/core.d.ts +132 -0
- package/dist/core.js +104 -0
- package/dist/core.js.map +1 -0
- package/dist/feedback.d.ts +28 -0
- package/dist/feedback.js +459 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +28 -0
- package/dist/proxy.js +12 -0
- package/dist/proxy.js.map +1 -0
- package/dist/referral.d.ts +86 -0
- package/dist/referral.js +128 -0
- package/dist/referral.js.map +1 -0
- package/dist/replay-core.d.ts +93 -0
- package/dist/replay-core.js +122 -0
- package/dist/replay-core.js.map +1 -0
- package/dist/replay.d.ts +34 -0
- package/dist/replay.js +200 -0
- package/dist/replay.js.map +1 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.js +43 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.d.ts +4 -0
- package/dist/styles.js +147 -0
- package/dist/styles.js.map +1 -0
- package/dist/survey.d.ts +4 -0
- package/dist/survey.js +201 -0
- package/dist/survey.js.map +1 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +77 -0
- package/dist/ui.js.map +1 -0
- package/package.json +96 -5
package/dist/capture.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/capture.ts
|
|
2
|
+
var INTERACTIVE = 'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="switch"],[onclick],[data-ck-goal]';
|
|
3
|
+
var MASK_SELECTOR = "[data-ck-mask]";
|
|
4
|
+
var BLOCK_ATTR_SELECTOR = "[data-ck-block]";
|
|
5
|
+
var generated = /\d{4,}|^[a-z]{1,3}-[a-z0-9]{5,}$|^_|__|css-|^sc-|^jsx-/i;
|
|
6
|
+
var part = (el) => {
|
|
7
|
+
const tag = el.tagName.toLowerCase();
|
|
8
|
+
if (el.id && !generated.test(el.id) && el.id.length <= 40) return `${tag}#${el.id}`;
|
|
9
|
+
const cls = typeof el.className === "string" ? el.className.split(/\s+/).filter((c) => c && !generated.test(c) && c.length <= 30 && !c.includes(":")) : [];
|
|
10
|
+
const base = cls.length ? `${tag}.${cls.slice(0, 2).join(".")}` : tag;
|
|
11
|
+
const same = el.parentElement ? [...el.parentElement.children].filter((c) => c.tagName === el.tagName) : [];
|
|
12
|
+
return same.length > 1 ? `${base}:nth-of-type(${same.indexOf(el) + 1})` : base;
|
|
13
|
+
};
|
|
14
|
+
var selectorOf = (el) => {
|
|
15
|
+
const out = [];
|
|
16
|
+
let cur = el;
|
|
17
|
+
while (cur && out.length < 3 && cur.tagName && cur.tagName !== "BODY" && cur.tagName !== "HTML") {
|
|
18
|
+
const p = part(cur);
|
|
19
|
+
out.unshift(p);
|
|
20
|
+
if (p.includes("#")) break;
|
|
21
|
+
cur = cur.parentElement;
|
|
22
|
+
}
|
|
23
|
+
return out.join(" > ").slice(0, 160);
|
|
24
|
+
};
|
|
25
|
+
var clean = (s) => (s ?? "").replace(/\s+/g, " ").trim().slice(0, 40);
|
|
26
|
+
var labelOf = (el) => {
|
|
27
|
+
if (el.closest(`${MASK_SELECTOR},${BLOCK_ATTR_SELECTOR}`)) return "";
|
|
28
|
+
const tag = el.tagName.toLowerCase();
|
|
29
|
+
if (tag === "input") {
|
|
30
|
+
const type = (el.getAttribute("type") ?? "text").toLowerCase();
|
|
31
|
+
return type === "submit" || type === "button" ? clean(el.getAttribute("aria-label") ?? el.value) : clean(el.getAttribute("aria-label"));
|
|
32
|
+
}
|
|
33
|
+
if (tag === "select" || tag === "textarea") return clean(el.getAttribute("aria-label"));
|
|
34
|
+
return clean(el.getAttribute("aria-label") ?? el.getAttribute("title") ?? el.innerText ?? el.textContent);
|
|
35
|
+
};
|
|
36
|
+
var clickInfo = (e) => {
|
|
37
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
38
|
+
if (!target) return null;
|
|
39
|
+
const hit = target.closest(INTERACTIVE);
|
|
40
|
+
const el = hit ?? target;
|
|
41
|
+
const doc = document.documentElement;
|
|
42
|
+
return {
|
|
43
|
+
x: Math.round(e.clientX),
|
|
44
|
+
y: Math.round(e.clientY + window.scrollY),
|
|
45
|
+
vw: window.innerWidth,
|
|
46
|
+
vh: window.innerHeight,
|
|
47
|
+
docH: Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
48
|
+
selector: selectorOf(el),
|
|
49
|
+
label: hit ? labelOf(hit) : "",
|
|
50
|
+
interactive: !!hit
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
var RageDetector = class {
|
|
54
|
+
constructor(windowMs = 1e3, radius = 30, count = 3) {
|
|
55
|
+
this.windowMs = windowMs;
|
|
56
|
+
this.radius = radius;
|
|
57
|
+
this.count = count;
|
|
58
|
+
}
|
|
59
|
+
windowMs;
|
|
60
|
+
radius;
|
|
61
|
+
count;
|
|
62
|
+
clicks = [];
|
|
63
|
+
fired = false;
|
|
64
|
+
push(x, y, t) {
|
|
65
|
+
const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
|
|
66
|
+
if (near.length !== this.clicks.length) this.fired = false;
|
|
67
|
+
this.clicks = [...near, { x, y, t }];
|
|
68
|
+
if (this.clicks.length >= this.count && !this.fired) {
|
|
69
|
+
this.fired = true;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var seenDepth = () => {
|
|
76
|
+
const doc = document.documentElement;
|
|
77
|
+
const h = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);
|
|
78
|
+
if (!h) return 0;
|
|
79
|
+
return Math.min(100, Math.round((window.scrollY + window.innerHeight) / h * 100));
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
BLOCK_ATTR_SELECTOR,
|
|
83
|
+
INTERACTIVE,
|
|
84
|
+
MASK_SELECTOR,
|
|
85
|
+
RageDetector,
|
|
86
|
+
clickInfo,
|
|
87
|
+
labelOf,
|
|
88
|
+
seenDepth,
|
|
89
|
+
selectorOf
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capture.ts"],"sourcesContent":["/**\n * Clicks and scroll depth for heatmaps, and the rage-click detector session recordings share. Small on purpose: it is\n * part of every page that uses the provider. Runs under the normal analytics consent; it never reads what anyone typed.\n *\n * A click is described by where it landed (x in CSS pixels with the viewport width, y in page pixels with the page\n * height), a short CSS selector and, for links and buttons only, a label of at most 40 characters. Nothing inside\n * `data-ck-mask` or `data-ck-block` gets a label, form fields never do.\n */\n\n/** Elements a click is meant for. A click on anything else is a dead click. */\nexport const INTERACTIVE =\n 'a[href],button,input,select,textarea,label,summary,[role=\"button\"],[role=\"link\"],[role=\"tab\"],[role=\"menuitem\"],[role=\"checkbox\"],[role=\"switch\"],[onclick],[data-ck-goal]';\n/** Text inside is masked in recordings and never becomes a click label. */\nexport const MASK_SELECTOR = \"[data-ck-mask]\";\n/** Left out of recordings entirely (a placeholder of the same size) and never labelled. */\nexport const BLOCK_ATTR_SELECTOR = \"[data-ck-block]\";\n\nexport interface ClickInfo {\n /** Pointer position: x in CSS pixels from the left of the viewport, y in page pixels from the top of the document. */\n x: number;\n y: number;\n /** Viewport width and height, and the page height, in CSS pixels. */\n vw: number;\n vh: number;\n docH: number;\n /** A short CSS path, like `main > section.pricing > button`. Ids and classes only, never text. */\n selector: string;\n /** For links and buttons: what they say (aria-label, title or text), at most 40 characters; else \"\". */\n label: string;\n /** On a link, button or field; false is a dead click. */\n interactive: boolean;\n}\n\n/** Ids and classes that look generated (hashes, long numbers) say nothing across builds. */\nconst generated = /\\d{4,}|^[a-z]{1,3}-[a-z0-9]{5,}$|^_|__|css-|^sc-|^jsx-/i;\n\nconst part = (el: Element) => {\n const tag = el.tagName.toLowerCase();\n if (el.id && !generated.test(el.id) && el.id.length <= 40) return `${tag}#${el.id}`;\n const cls = typeof el.className === \"string\" ? el.className.split(/\\s+/).filter((c) => c && !generated.test(c) && c.length <= 30 && !c.includes(\":\")) : [];\n const base = cls.length ? `${tag}.${cls.slice(0, 2).join(\".\")}` : tag;\n // Siblings of the same kind (two plain buttons side by side) are told apart by their place.\n const same = el.parentElement ? [...el.parentElement.children].filter((c) => c.tagName === el.tagName) : [];\n return same.length > 1 ? `${base}:nth-of-type(${same.indexOf(el) + 1})` : base;\n};\n\n/** A selector for the element, three levels at most; stops early at an id. */\nexport const selectorOf = (el: Element) => {\n const out: string[] = [];\n let cur: Element | null = el;\n while (cur && out.length < 3 && cur.tagName && cur.tagName !== \"BODY\" && cur.tagName !== \"HTML\") {\n const p = part(cur);\n out.unshift(p);\n if (p.includes(\"#\")) break;\n cur = cur.parentElement;\n }\n return out.join(\" > \").slice(0, 160);\n};\n\nconst clean = (s: string | null | undefined) => (s ?? \"\").replace(/\\s+/g, \" \").trim().slice(0, 40);\n\n/** What a link or button says, never a field's value; \"\" inside masked or blocked parts. */\nexport const labelOf = (el: Element) => {\n if (el.closest(`${MASK_SELECTOR},${BLOCK_ATTR_SELECTOR}`)) return \"\";\n const tag = el.tagName.toLowerCase();\n if (tag === \"input\") {\n const type = (el.getAttribute(\"type\") ?? \"text\").toLowerCase();\n return type === \"submit\" || type === \"button\" ? clean(el.getAttribute(\"aria-label\") ?? (el as HTMLInputElement).value) : clean(el.getAttribute(\"aria-label\"));\n }\n if (tag === \"select\" || tag === \"textarea\") return clean(el.getAttribute(\"aria-label\"));\n return clean(el.getAttribute(\"aria-label\") ?? el.getAttribute(\"title\") ?? (el as HTMLElement).innerText ?? el.textContent);\n};\n\n/** The click's description, or null when it did not land on an element. */\nexport const clickInfo = (e: MouseEvent): ClickInfo | null => {\n const target = e.target instanceof Element ? e.target : null;\n if (!target) return null;\n const hit = target.closest(INTERACTIVE);\n const el = hit ?? target;\n const doc = document.documentElement;\n return {\n x: Math.round(e.clientX),\n y: Math.round(e.clientY + window.scrollY),\n vw: window.innerWidth,\n vh: window.innerHeight,\n docH: Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0),\n selector: selectorOf(el),\n label: hit ? labelOf(hit) : \"\",\n interactive: !!hit,\n };\n};\n\n/**\n * Three or more clicks within one second on the same spot (30 pixels) are a rage click. `push` answers true once per\n * burst, on the third click.\n */\nexport class RageDetector {\n private clicks: { x: number; y: number; t: number }[] = [];\n private fired = false;\n constructor(\n private readonly windowMs = 1000,\n private readonly radius = 30,\n private readonly count = 3,\n ) {}\n push(x: number, y: number, t: number) {\n const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);\n if (near.length !== this.clicks.length) this.fired = false;\n this.clicks = [...near, { x, y, t }];\n if (this.clicks.length >= this.count && !this.fired) {\n this.fired = true;\n return true;\n }\n return false;\n }\n}\n\n/** How far down the page the visitor has seen, 0 to 100. */\nexport const seenDepth = () => {\n const doc = document.documentElement;\n const h = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);\n if (!h) return 0;\n return Math.min(100, Math.round(((window.scrollY + window.innerHeight) / h) * 100));\n};\n"],"mappings":"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","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type AttributionMode } from "./referral.js";
|
|
3
|
+
import { type CockpitConfig, type ConsentChoice, type ConsentMode, type VariantId } from "./core.js";
|
|
4
|
+
export interface CockpitProviderProps {
|
|
5
|
+
/** The product's public key from Cockpit › Settings › SDK. Safe in the browser. Default: `NEXT_PUBLIC_MRROCKET_KEY`. */
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
/** Where MRRocket runs, like `https://app.example.com`. Default: `NEXT_PUBLIC_MRROCKET_HOST`, else the page's own origin. */
|
|
8
|
+
host?: string;
|
|
9
|
+
preview?: Record<string, VariantId>;
|
|
10
|
+
embedded?: boolean;
|
|
11
|
+
/** Config fetched on the server, to render the right variant on the first paint. */
|
|
12
|
+
config?: CockpitConfig;
|
|
13
|
+
/** Your own id for this visitor, for example a signed-in user id. Keeps variants stable without cookies. */
|
|
14
|
+
distinctId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* "cookieless" (default) stores nothing in the browser, "cookies" keeps a visitor cookie for a year, "off" sends
|
|
17
|
+
* nothing. The SDK does not remember the choice: set it on every page load, from the prop or setConsent().
|
|
18
|
+
*/
|
|
19
|
+
consent?: ConsentMode;
|
|
20
|
+
/** Send nothing when the browser asks not to be tracked (Do Not Track or Global Privacy Control). */
|
|
21
|
+
respectDnt?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The visitor's consent to session recordings, separate from analytics (default false). Recording also needs the
|
|
24
|
+
* product to record (Cockpit › Sessions) and analytics not "off". Without it nothing is recorded or buffered.
|
|
25
|
+
* Change it later with `setConsent({ replay })`.
|
|
26
|
+
*/
|
|
27
|
+
replay?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Clicks and scroll depth for Heatmaps (default true): where a click landed, a short selector and a link's or
|
|
30
|
+
* button's label, never typed text. Sent under the normal analytics consent as `$click`, `$rageclick` and `$scroll`.
|
|
31
|
+
*/
|
|
32
|
+
heatmaps?: boolean;
|
|
33
|
+
/** Ask visitors the surveys you run in Cockpit. Set false to switch them off. */
|
|
34
|
+
surveys?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Affiliate referrals (`?ref=` links and MRRocket's tracked links). "consent" (default): stored in the first-party
|
|
37
|
+
* `mrr_ref` cookie only while consent is "cookies", in memory otherwise. "always": stored whatever the consent (only
|
|
38
|
+
* where your legal basis allows it). "off": referrals are ignored.
|
|
39
|
+
*/
|
|
40
|
+
attribution?: AttributionMode;
|
|
41
|
+
/** The query parameter of direct partner links, default `ref` (`https://your.site/?ref=maya`). */
|
|
42
|
+
referralParam?: string;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Put it once around your app, in `app/layout.tsx`. It loads your experiments, keeps each visitor in one variant,
|
|
47
|
+
* records page views and sends your events to Cockpit in small batches.
|
|
48
|
+
*
|
|
49
|
+
* `preview` forces variants for this render, for example from the `ck_preview` search param on the server,
|
|
50
|
+
* so a Cockpit preview never flickers. Without it the provider reads the param itself after the first render.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CockpitProvider: ({ apiKey: apiKeyProp, host: hostProp, preview: forced, embedded: embeddedProp, config: bootstrapConfig, distinctId: bootstrapId, consent: consentProp, respectDnt, replay: replayProp, heatmaps, surveys, attribution, referralParam, children, }: CockpitProviderProps) => import("react").JSX.Element;
|
|
53
|
+
/**
|
|
54
|
+
* `track`, `identify`, the consent mode, the replay consent and their setter, the visitor id and whether the
|
|
55
|
+
* experiments have loaded. `setConsent("cookies")` sets the mode, `setConsent({ replay: true })` the replay consent.
|
|
56
|
+
*/
|
|
57
|
+
export declare const useCockpit: () => {
|
|
58
|
+
track: (event: string, props?: Record<string, unknown>) => void;
|
|
59
|
+
identify: (id: string, props?: Record<string, unknown>) => void;
|
|
60
|
+
distinctId: string;
|
|
61
|
+
ready: boolean;
|
|
62
|
+
embedded: boolean;
|
|
63
|
+
consent: ConsentMode;
|
|
64
|
+
replay: boolean;
|
|
65
|
+
setConsent: (choice: ConsentChoice) => void;
|
|
66
|
+
};
|
|
67
|
+
/** The variant this visitor sees, for example `"control"` or `"a"`. Records the exposure. */
|
|
68
|
+
export declare const useVariant: (key: string) => VariantId;
|
|
69
|
+
/** Renders the element for the visitor's variant; control when the variant has no element. */
|
|
70
|
+
export declare const Experiment: ({ name, variants }: {
|
|
71
|
+
name: string;
|
|
72
|
+
variants: Record<VariantId, ReactNode> & {
|
|
73
|
+
control: ReactNode;
|
|
74
|
+
};
|
|
75
|
+
}) => import("react").JSX.Element;
|
|
76
|
+
/**
|
|
77
|
+
* A text that experiments may change without a deploy: Cockpit sends the new copy for `id` and this renders it.
|
|
78
|
+
* Write your default as the children.
|
|
79
|
+
*/
|
|
80
|
+
export declare const Text: ({ id, children }: {
|
|
81
|
+
id: string;
|
|
82
|
+
children: string;
|
|
83
|
+
}) => import("react").JSX.Element;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/client.tsx
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { Ctx, useCtx } from "./context.js";
|
|
7
|
+
import { Survey, forgetStoredDismissals } from "./survey.js";
|
|
8
|
+
import { RageDetector, clickInfo, seenDepth } from "./capture.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_REFERRAL_DAYS,
|
|
11
|
+
captureReferral,
|
|
12
|
+
carriesReferral,
|
|
13
|
+
forgetStoredReferral,
|
|
14
|
+
getReferral,
|
|
15
|
+
resetReferral,
|
|
16
|
+
storeReferral
|
|
17
|
+
} from "./referral.js";
|
|
18
|
+
import {
|
|
19
|
+
EMBED_PARAM,
|
|
20
|
+
ID_COOKIE,
|
|
21
|
+
PREVIEW_PARAM,
|
|
22
|
+
SESSION_COOKIE,
|
|
23
|
+
SESSION_TIMEOUT_MS,
|
|
24
|
+
envKey,
|
|
25
|
+
newDistinctId,
|
|
26
|
+
parsePreview,
|
|
27
|
+
readUtm,
|
|
28
|
+
resolveHost,
|
|
29
|
+
resolveVariant,
|
|
30
|
+
textChanges
|
|
31
|
+
} from "./core.js";
|
|
32
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
33
|
+
var YEAR_S = 60 * 60 * 24 * 365;
|
|
34
|
+
var MAX_BATCH = 100;
|
|
35
|
+
var MAX_BYTES = 6e4;
|
|
36
|
+
var readCookie = (name) => {
|
|
37
|
+
if (typeof document === "undefined") return void 0;
|
|
38
|
+
const raw = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
39
|
+
if (!raw) return void 0;
|
|
40
|
+
try {
|
|
41
|
+
return decodeURIComponent(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var writeCookie = (name, value, maxAge) => {
|
|
47
|
+
const secure = window.location.protocol === "https:" ? "; secure" : "";
|
|
48
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
|
|
49
|
+
};
|
|
50
|
+
var deleteCookie = (name) => {
|
|
51
|
+
document.cookie = `${name}=; path=/; max-age=0; samesite=lax`;
|
|
52
|
+
};
|
|
53
|
+
var page = { utm: {} };
|
|
54
|
+
var memoryId = () => page.id ??= newDistinctId();
|
|
55
|
+
var visitorId = (mode, write) => {
|
|
56
|
+
if (mode !== "cookies") return memoryId();
|
|
57
|
+
const stored = readCookie(ID_COOKIE);
|
|
58
|
+
if (stored) return page.id = stored;
|
|
59
|
+
const id = memoryId();
|
|
60
|
+
if (write) writeCookie(ID_COOKIE, id, YEAR_S);
|
|
61
|
+
return id;
|
|
62
|
+
};
|
|
63
|
+
var touchSession = (persist) => {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const s = page.session;
|
|
66
|
+
const id = (persist ? readCookie(SESSION_COOKIE) : void 0) ?? (s && now - s.last < SESSION_TIMEOUT_MS ? s.id : newDistinctId());
|
|
67
|
+
page.session = { id, last: now };
|
|
68
|
+
if (persist) writeCookie(SESSION_COOKIE, id, SESSION_TIMEOUT_MS / 1e3);
|
|
69
|
+
return id;
|
|
70
|
+
};
|
|
71
|
+
var enterPage = () => {
|
|
72
|
+
const href = window.location.href;
|
|
73
|
+
if (page.url === href) return;
|
|
74
|
+
page.referrer = page.url ?? (document.referrer || void 0);
|
|
75
|
+
page.url = href;
|
|
76
|
+
const utm = readUtm(window.location.search);
|
|
77
|
+
if (Object.keys(utm).length) page.utm = utm;
|
|
78
|
+
};
|
|
79
|
+
var privacySignal = () => {
|
|
80
|
+
const nav = navigator;
|
|
81
|
+
const win = window;
|
|
82
|
+
return nav.doNotTrack === "1" || win.doNotTrack === "1" || nav.globalPrivacyControl === true;
|
|
83
|
+
};
|
|
84
|
+
var warned = /* @__PURE__ */ new Set();
|
|
85
|
+
var warnOnce = (message) => {
|
|
86
|
+
if (warned.has(message)) return;
|
|
87
|
+
warned.add(message);
|
|
88
|
+
console.warn(`[MRRocket] ${message}`);
|
|
89
|
+
};
|
|
90
|
+
var byteLength = (s) => new TextEncoder().encode(s).length;
|
|
91
|
+
var bodies = (key, events) => {
|
|
92
|
+
const head = `{"key":${JSON.stringify(key)},"events":[`;
|
|
93
|
+
const empty = byteLength(head) + 2;
|
|
94
|
+
const out = [];
|
|
95
|
+
let chunk = [];
|
|
96
|
+
let size = empty;
|
|
97
|
+
const close = () => {
|
|
98
|
+
if (chunk.length) out.push(`${head}${chunk.join(",")}]}`);
|
|
99
|
+
chunk = [];
|
|
100
|
+
size = empty;
|
|
101
|
+
};
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
const json = JSON.stringify(event);
|
|
104
|
+
const n = byteLength(json) + 1;
|
|
105
|
+
if (empty + n > MAX_BYTES) continue;
|
|
106
|
+
if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
|
|
107
|
+
chunk.push(json);
|
|
108
|
+
size += n;
|
|
109
|
+
}
|
|
110
|
+
close();
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
var CockpitProvider = ({
|
|
114
|
+
apiKey: apiKeyProp,
|
|
115
|
+
host: hostProp,
|
|
116
|
+
preview: forced,
|
|
117
|
+
embedded: embeddedProp,
|
|
118
|
+
config: bootstrapConfig,
|
|
119
|
+
distinctId: bootstrapId,
|
|
120
|
+
consent: consentProp = "cookieless",
|
|
121
|
+
respectDnt = false,
|
|
122
|
+
replay: replayProp = false,
|
|
123
|
+
heatmaps = true,
|
|
124
|
+
surveys = true,
|
|
125
|
+
attribution = "consent",
|
|
126
|
+
referralParam = "ref",
|
|
127
|
+
children
|
|
128
|
+
}) => {
|
|
129
|
+
const pathname = usePathname();
|
|
130
|
+
const apiKey = apiKeyProp ?? envKey() ?? "";
|
|
131
|
+
const host = resolveHost(hostProp);
|
|
132
|
+
const [config, setConfig] = useState(bootstrapConfig ?? null);
|
|
133
|
+
const [consent, setConsentState] = useState(consentProp);
|
|
134
|
+
const [replayConsent, setReplayConsent] = useState(replayProp);
|
|
135
|
+
const [mounted, setMounted] = useState(false);
|
|
136
|
+
const [dnt, setDnt] = useState(false);
|
|
137
|
+
const [visitor, setVisitor] = useState(null);
|
|
138
|
+
const [identified, setIdentified] = useState();
|
|
139
|
+
const [urlPreview, setUrlPreview] = useState({});
|
|
140
|
+
const [urlEmbedded, setUrlEmbedded] = useState(false);
|
|
141
|
+
const [user, setUser] = useState();
|
|
142
|
+
const [ready, setReady] = useState(!!bootstrapConfig);
|
|
143
|
+
const queue = useRef([]);
|
|
144
|
+
const early = useRef([]);
|
|
145
|
+
const exposed = useRef(/* @__PURE__ */ new Set());
|
|
146
|
+
const lastPage = useRef(null);
|
|
147
|
+
const bootstrap = bootstrapId && bootstrapId !== "anonymous" ? bootstrapId : void 0;
|
|
148
|
+
const preview = useMemo(() => ({ ...urlPreview, ...forced }), [urlPreview, forced]);
|
|
149
|
+
const embedded = embeddedProp ?? urlEmbedded;
|
|
150
|
+
const effectiveConsent = respectDnt && dnt ? "off" : consent;
|
|
151
|
+
const distinctId = identified ?? bootstrap ?? visitor?.id ?? "anonymous";
|
|
152
|
+
const tracking = !embedded && effectiveConsent !== "off" && visitor?.mode === effectiveConsent;
|
|
153
|
+
const sendId = effectiveConsent === "cookies" ? distinctId : identified;
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const params = new URLSearchParams(window.location.search);
|
|
156
|
+
setUrlPreview(parsePreview(params.get(PREVIEW_PARAM)));
|
|
157
|
+
setUrlEmbedded(params.get(EMBED_PARAM) === "1");
|
|
158
|
+
setDnt(privacySignal());
|
|
159
|
+
setMounted(true);
|
|
160
|
+
}, []);
|
|
161
|
+
const setConsent = useCallback((choice) => {
|
|
162
|
+
const { analytics: mode, replay } = typeof choice === "string" ? { analytics: choice, replay: void 0 } : choice;
|
|
163
|
+
if (mode === "off") queue.current.length = 0;
|
|
164
|
+
if (mode) setConsentState(mode);
|
|
165
|
+
if (typeof replay === "boolean") setReplayConsent(replay);
|
|
166
|
+
}, []);
|
|
167
|
+
const lastProp = useRef(consentProp);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (lastProp.current === consentProp) return;
|
|
170
|
+
lastProp.current = consentProp;
|
|
171
|
+
setConsent(consentProp);
|
|
172
|
+
}, [consentProp, setConsent]);
|
|
173
|
+
const lastReplayProp = useRef(replayProp);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (lastReplayProp.current === replayProp) return;
|
|
176
|
+
lastReplayProp.current = replayProp;
|
|
177
|
+
setConsent({ replay: replayProp });
|
|
178
|
+
}, [replayProp, setConsent]);
|
|
179
|
+
const storedFor = useRef(null);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!mounted) return;
|
|
182
|
+
const before = storedFor.current;
|
|
183
|
+
storedFor.current = effectiveConsent;
|
|
184
|
+
if (before === "cookies" && effectiveConsent !== "cookies") {
|
|
185
|
+
deleteCookie(ID_COOKIE);
|
|
186
|
+
deleteCookie(SESSION_COOKIE);
|
|
187
|
+
forgetStoredDismissals();
|
|
188
|
+
}
|
|
189
|
+
if (effectiveConsent === "off") {
|
|
190
|
+
queue.current.length = 0;
|
|
191
|
+
page.session = void 0;
|
|
192
|
+
}
|
|
193
|
+
setVisitor({ mode: effectiveConsent, id: visitorId(effectiveConsent, !bootstrap) });
|
|
194
|
+
}, [mounted, effectiveConsent, bootstrap]);
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!apiKey) warnOnce("No public key. Pass apiKey or set NEXT_PUBLIC_MRROCKET_KEY; nothing is tracked until then.");
|
|
197
|
+
if (bootstrapConfig || !apiKey) {
|
|
198
|
+
setReady(true);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let live2 = true;
|
|
202
|
+
fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`).then((r) => {
|
|
203
|
+
if (!r.ok) warnOnce(`Config request to ${host || "this origin"} answered ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);
|
|
204
|
+
return r.ok ? r.json() : null;
|
|
205
|
+
}).then((c) => {
|
|
206
|
+
if (!live2) return;
|
|
207
|
+
if (c && Array.isArray(c.experiments)) setConfig(c);
|
|
208
|
+
setReady(true);
|
|
209
|
+
}).catch(() => {
|
|
210
|
+
warnOnce(`Could not reach ${host || "this origin"}. Check NEXT_PUBLIC_MRROCKET_HOST. The page keeps working with control variants.`);
|
|
211
|
+
if (live2) setReady(true);
|
|
212
|
+
});
|
|
213
|
+
return () => {
|
|
214
|
+
live2 = false;
|
|
215
|
+
};
|
|
216
|
+
}, [apiKey, host, bootstrapConfig]);
|
|
217
|
+
const flush = useCallback(
|
|
218
|
+
(beacon = false) => {
|
|
219
|
+
if (!queue.current.length) return;
|
|
220
|
+
if (!apiKey) {
|
|
221
|
+
queue.current.length = 0;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const url = `${host}/api/sdk/events`;
|
|
225
|
+
for (const body of bodies(apiKey, queue.current.splice(0))) {
|
|
226
|
+
const sent = beacon && typeof navigator !== "undefined" && "sendBeacon" in navigator && navigator.sendBeacon(url, body);
|
|
227
|
+
if (!sent)
|
|
228
|
+
fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).then((r) => {
|
|
229
|
+
if (!r.ok) warnOnce(`Events were rejected with ${r.status}. Check the public key (Settings \u203A SDK) and NEXT_PUBLIC_MRROCKET_HOST.`);
|
|
230
|
+
}).catch(() => warnOnce(`Could not reach ${host || "this origin"} to send events. Check NEXT_PUBLIC_MRROCKET_HOST.`));
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[apiKey, host]
|
|
234
|
+
);
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const timer = window.setInterval(() => flush(), 2e3);
|
|
237
|
+
const leave = () => flush(true);
|
|
238
|
+
window.addEventListener("pagehide", leave);
|
|
239
|
+
return () => {
|
|
240
|
+
window.clearInterval(timer);
|
|
241
|
+
window.removeEventListener("pagehide", leave);
|
|
242
|
+
flush(true);
|
|
243
|
+
};
|
|
244
|
+
}, [flush]);
|
|
245
|
+
const live = useRef({ resolved: false, tracking, distinctId, sendId, persist: false, identified: false });
|
|
246
|
+
live.current = { resolved: visitor !== null, tracking, distinctId, sendId, persist: visitor?.mode === "cookies", identified: !!identified };
|
|
247
|
+
const record = useCallback((event, props, ts = Date.now(), url) => {
|
|
248
|
+
const now = live.current;
|
|
249
|
+
if (!now.resolved) {
|
|
250
|
+
early.current.push(() => record(event, props, ts, url));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!now.tracking) return;
|
|
254
|
+
const referral = carriesReferral(event) ? getReferral() : null;
|
|
255
|
+
queue.current.push({
|
|
256
|
+
event,
|
|
257
|
+
props: referral ? { ...props, $referral: { code: referral.code, clickId: referral.clickId } } : props,
|
|
258
|
+
distinctId: now.sendId,
|
|
259
|
+
sessionId: touchSession(now.persist),
|
|
260
|
+
ts,
|
|
261
|
+
url: url ?? window.location.href,
|
|
262
|
+
referrer: page.referrer,
|
|
263
|
+
...page.utm,
|
|
264
|
+
screen: { w: window.screen.width, h: window.screen.height }
|
|
265
|
+
});
|
|
266
|
+
}, []);
|
|
267
|
+
const replayer = useRef(null);
|
|
268
|
+
const track = useCallback(
|
|
269
|
+
(event, props) => {
|
|
270
|
+
record(event, props);
|
|
271
|
+
if (live.current.tracking) replayer.current?.event(event);
|
|
272
|
+
},
|
|
273
|
+
[record]
|
|
274
|
+
);
|
|
275
|
+
const identify = useCallback(
|
|
276
|
+
(id, props) => {
|
|
277
|
+
const now = live.current;
|
|
278
|
+
if (!now.resolved) {
|
|
279
|
+
early.current.push(() => identify(id, props));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const previous = now.sendId;
|
|
283
|
+
live.current = { ...now, distinctId: id, sendId: id, identified: true };
|
|
284
|
+
record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
|
|
285
|
+
replayer.current?.identify(id);
|
|
286
|
+
if (now.persist) writeCookie(ID_COOKIE, id, YEAR_S);
|
|
287
|
+
setIdentified(id);
|
|
288
|
+
setUser({ id, ...props });
|
|
289
|
+
},
|
|
290
|
+
[record]
|
|
291
|
+
);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!visitor) return;
|
|
294
|
+
for (const run of early.current.splice(0)) run();
|
|
295
|
+
}, [visitor]);
|
|
296
|
+
const referralDays = config?.referral?.cookieDays ?? DEFAULT_REFERRAL_DAYS;
|
|
297
|
+
const storeReferrals = attribution === "always" ? effectiveConsent !== "off" : attribution === "consent" && effectiveConsent === "cookies";
|
|
298
|
+
const captured = useRef(null);
|
|
299
|
+
const storedReferral = useRef(false);
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!visitor || !ready || embedded) return;
|
|
302
|
+
if (attribution === "off" || effectiveConsent === "off") {
|
|
303
|
+
resetReferral();
|
|
304
|
+
if (attribution === "off" || storedReferral.current) forgetStoredReferral();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const href = window.location.href;
|
|
308
|
+
if (captured.current === href) return;
|
|
309
|
+
captured.current = href;
|
|
310
|
+
void captureReferral({
|
|
311
|
+
search: window.location.search,
|
|
312
|
+
param: referralParam,
|
|
313
|
+
store: storeReferrals,
|
|
314
|
+
days: referralDays,
|
|
315
|
+
beacon: apiKey ? ({ code, clickId }) => fetch(`${host}/api/sdk/affiliate/click`, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "content-type": "application/json" },
|
|
318
|
+
body: JSON.stringify({ key: apiKey, code, clickId, url: href, referrer: document.referrer || void 0 }),
|
|
319
|
+
keepalive: true
|
|
320
|
+
}).then((r) => r.ok ? r.json() : null).then((b) => b?.clickId ?? null) : void 0
|
|
321
|
+
});
|
|
322
|
+
}, [visitor, ready, embedded, attribution, effectiveConsent, pathname, referralParam, storeReferrals, referralDays, apiKey, host]);
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (!mounted) return;
|
|
325
|
+
const r = getReferral();
|
|
326
|
+
if (storeReferrals && r) storeReferral(r, referralDays);
|
|
327
|
+
else if (!storeReferrals && storedReferral.current) forgetStoredReferral();
|
|
328
|
+
storedReferral.current = storeReferrals;
|
|
329
|
+
}, [mounted, storeReferrals, referralDays]);
|
|
330
|
+
const expose = useCallback(
|
|
331
|
+
(key, variant) => {
|
|
332
|
+
if (!tracking) return;
|
|
333
|
+
const mark = `${pathname}|${key}`;
|
|
334
|
+
if (exposed.current.has(mark)) return;
|
|
335
|
+
exposed.current.add(mark);
|
|
336
|
+
track("$exposure", { experiment: key, variant });
|
|
337
|
+
},
|
|
338
|
+
[pathname, track, tracking]
|
|
339
|
+
);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
enterPage();
|
|
342
|
+
replayer.current?.page(pathname);
|
|
343
|
+
}, [pathname]);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!tracking || lastPage.current === pathname) return;
|
|
346
|
+
lastPage.current = pathname;
|
|
347
|
+
track("$pageview", { path: pathname });
|
|
348
|
+
}, [pathname, tracking, track]);
|
|
349
|
+
const rage = useRef(new RageDetector());
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
const onClick = (e) => {
|
|
352
|
+
const el = e.target?.closest("[data-ck-goal]");
|
|
353
|
+
if (el?.dataset.ckGoal) track(el.dataset.ckGoal, { text: el.textContent?.trim().slice(0, 80) });
|
|
354
|
+
if (!live.current.tracking) return;
|
|
355
|
+
const c = clickInfo(e);
|
|
356
|
+
if (!c) return;
|
|
357
|
+
const raged = rage.current.push(c.x, c.y, Date.now());
|
|
358
|
+
replayer.current?.click(c, raged);
|
|
359
|
+
if (!heatmaps) return;
|
|
360
|
+
const props = { x: c.x, y: c.y, vw: c.vw, vh: c.vh, docH: c.docH, selector: c.selector, label: c.label, dead: !c.interactive };
|
|
361
|
+
record("$click", props);
|
|
362
|
+
if (raged) record("$rageclick", props);
|
|
363
|
+
};
|
|
364
|
+
document.addEventListener("click", onClick);
|
|
365
|
+
return () => document.removeEventListener("click", onClick);
|
|
366
|
+
}, [track, record, heatmaps]);
|
|
367
|
+
const depth = useRef(null);
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
if (!heatmaps || !tracking || embedded) return;
|
|
370
|
+
const send = () => {
|
|
371
|
+
const d = depth.current;
|
|
372
|
+
if (d && d.max > 0) record("$scroll", { depth: d.max, docH: d.docH, vw: window.innerWidth, vh: window.innerHeight }, Date.now(), d.url);
|
|
373
|
+
depth.current = null;
|
|
374
|
+
};
|
|
375
|
+
const measure = () => {
|
|
376
|
+
const doc = document.documentElement;
|
|
377
|
+
const docH = Math.max(doc.scrollHeight, document.body?.scrollHeight ?? 0);
|
|
378
|
+
const max = seenDepth();
|
|
379
|
+
const href = window.location.href;
|
|
380
|
+
if (depth.current?.url !== href) depth.current = { url: href, max, docH };
|
|
381
|
+
else if (max > depth.current.max) depth.current = { url: href, max, docH };
|
|
382
|
+
};
|
|
383
|
+
measure();
|
|
384
|
+
let frame = 0;
|
|
385
|
+
const onScroll = () => {
|
|
386
|
+
if (frame) return;
|
|
387
|
+
frame = window.requestAnimationFrame(() => {
|
|
388
|
+
frame = 0;
|
|
389
|
+
measure();
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
const onHide = () => {
|
|
393
|
+
send();
|
|
394
|
+
flush(true);
|
|
395
|
+
};
|
|
396
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
397
|
+
window.addEventListener("pagehide", onHide);
|
|
398
|
+
return () => {
|
|
399
|
+
window.removeEventListener("scroll", onScroll);
|
|
400
|
+
window.removeEventListener("pagehide", onHide);
|
|
401
|
+
window.cancelAnimationFrame(frame);
|
|
402
|
+
send();
|
|
403
|
+
};
|
|
404
|
+
}, [heatmaps, tracking, embedded, pathname, record, flush]);
|
|
405
|
+
const replayConfig = config?.replay;
|
|
406
|
+
const recording = tracking && replayConsent && !!replayConfig && !!apiKey;
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (!recording || !replayConfig) return;
|
|
409
|
+
let active = true;
|
|
410
|
+
let handle = null;
|
|
411
|
+
void (typeof window === "undefined" ? Promise.resolve(null) : import("./replay.js")).then(
|
|
412
|
+
(m) => m?.startReplay({
|
|
413
|
+
host,
|
|
414
|
+
apiKey,
|
|
415
|
+
sessionId: touchSession(live.current.persist),
|
|
416
|
+
distinctId: live.current.sendId,
|
|
417
|
+
identified: live.current.identified,
|
|
418
|
+
sampleRate: replayConfig.sampleRate,
|
|
419
|
+
warn: warnOnce
|
|
420
|
+
})
|
|
421
|
+
).then((h) => {
|
|
422
|
+
if (!active) h?.stop(true);
|
|
423
|
+
else replayer.current = handle = h ?? null;
|
|
424
|
+
}).catch(() => warnOnce("Session recording could not start (the recorder did not load)."));
|
|
425
|
+
return () => {
|
|
426
|
+
active = false;
|
|
427
|
+
handle?.stop(true);
|
|
428
|
+
if (replayer.current === handle) replayer.current = null;
|
|
429
|
+
};
|
|
430
|
+
}, [recording, replayConfig, host, apiKey]);
|
|
431
|
+
const texts = useMemo(() => textChanges(config, distinctId, preview), [config, distinctId, preview]);
|
|
432
|
+
const value = useMemo(
|
|
433
|
+
() => ({ apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, replayConsent, setConsent, texts, track, identify, expose }),
|
|
434
|
+
[apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, replayConsent, setConsent, texts, track, identify, expose]
|
|
435
|
+
);
|
|
436
|
+
return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
|
|
437
|
+
children,
|
|
438
|
+
surveys ? /* @__PURE__ */ jsx(Survey, {}) : null
|
|
439
|
+
] });
|
|
440
|
+
};
|
|
441
|
+
var useCockpit = () => {
|
|
442
|
+
const { track, identify, distinctId, ready, embedded, consent, replayConsent, setConsent } = useCtx();
|
|
443
|
+
return { track, identify, distinctId, ready, embedded, consent, replay: replayConsent, setConsent };
|
|
444
|
+
};
|
|
445
|
+
var useVariant = (key) => {
|
|
446
|
+
const { config, distinctId, preview, expose } = useCtx();
|
|
447
|
+
const variant = resolveVariant(key, config, distinctId, preview);
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (config || preview[key]) expose(key, variant);
|
|
450
|
+
}, [config, preview, key, variant, expose]);
|
|
451
|
+
return variant;
|
|
452
|
+
};
|
|
453
|
+
var Experiment = ({ name, variants }) => {
|
|
454
|
+
const variant = useVariant(name);
|
|
455
|
+
return /* @__PURE__ */ jsx(Fragment, { children: variants[variant] ?? variants.control });
|
|
456
|
+
};
|
|
457
|
+
var Text = ({ id, children }) => {
|
|
458
|
+
const { texts } = useCtx();
|
|
459
|
+
return /* @__PURE__ */ jsx(Fragment, { children: texts[id] ?? children });
|
|
460
|
+
};
|
|
461
|
+
export {
|
|
462
|
+
CockpitProvider,
|
|
463
|
+
Experiment,
|
|
464
|
+
Text,
|
|
465
|
+
useCockpit,
|
|
466
|
+
useVariant
|
|
467
|
+
};
|
|
468
|
+
//# sourceMappingURL=client.js.map
|