@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/proxy.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */
|
|
2
|
+
interface IncomingCookies {
|
|
3
|
+
get(name: string): {
|
|
4
|
+
value: string;
|
|
5
|
+
} | undefined;
|
|
6
|
+
set(name: string, value: string): unknown;
|
|
7
|
+
}
|
|
8
|
+
/** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */
|
|
9
|
+
interface OutgoingCookies {
|
|
10
|
+
set(name: string, value: string, options?: {
|
|
11
|
+
path?: string;
|
|
12
|
+
maxAge?: number;
|
|
13
|
+
sameSite?: "lax" | "strict" | "none";
|
|
14
|
+
}): unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns
|
|
18
|
+
* the variant they will keep. It writes a cookie, so only use it with `consent="cookies"` and only once the visitor
|
|
19
|
+
* agreed; in the default cookieless mode leave it out.
|
|
20
|
+
*
|
|
21
|
+
* export function proxy(request: NextRequest) {
|
|
22
|
+
* const response = NextResponse.next();
|
|
23
|
+
* withCockpitId(request.cookies, response.cookies);
|
|
24
|
+
* return response;
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
export declare const withCockpitId: (incoming: IncomingCookies, outgoing: OutgoingCookies) => void;
|
|
28
|
+
export {};
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/proxy.ts
|
|
2
|
+
import { ID_COOKIE, newDistinctId } from "./core.js";
|
|
3
|
+
var withCockpitId = (incoming, outgoing) => {
|
|
4
|
+
if (incoming.get(ID_COOKIE)) return;
|
|
5
|
+
const id = newDistinctId();
|
|
6
|
+
incoming.set(ID_COOKIE, id);
|
|
7
|
+
outgoing.set(ID_COOKIE, id, { path: "/", maxAge: 60 * 60 * 24 * 365, sameSite: "lax" });
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
withCockpitId
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proxy.ts"],"sourcesContent":["import { ID_COOKIE, newDistinctId } from \"./core\";\n\n/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */\ninterface IncomingCookies {\n get(name: string): { value: string } | undefined;\n set(name: string, value: string): unknown;\n}\n\n/** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */\ninterface OutgoingCookies {\n set(name: string, value: string, options?: { path?: string; maxAge?: number; sameSite?: \"lax\" | \"strict\" | \"none\" }): unknown;\n}\n\n/**\n * For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns\n * the variant they will keep. It writes a cookie, so only use it with `consent=\"cookies\"` and only once the visitor\n * agreed; in the default cookieless mode leave it out.\n *\n * export function proxy(request: NextRequest) {\n * const response = NextResponse.next();\n * withCockpitId(request.cookies, response.cookies);\n * return response;\n * }\n */\nexport const withCockpitId = (incoming: IncomingCookies, outgoing: OutgoingCookies) => {\n if (incoming.get(ID_COOKIE)) return;\n const id = newDistinctId();\n incoming.set(ID_COOKIE, id);\n outgoing.set(ID_COOKIE, id, { path: \"/\", maxAge: 60 * 60 * 24 * 365, sameSite: \"lax\" });\n};\n"],"mappings":";AAAA,SAAS,WAAW,qBAAqB;AAwBlC,IAAM,gBAAgB,CAAC,UAA2B,aAA8B;AACnF,MAAI,SAAS,IAAI,SAAS,EAAG;AAC7B,QAAM,KAAK,cAAc;AACzB,WAAS,IAAI,WAAW,EAAE;AAC1B,WAAS,IAAI,WAAW,IAAI,EAAE,MAAM,KAAK,QAAQ,KAAK,KAAK,KAAK,KAAK,UAAU,MAAM,CAAC;AAC1F;","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affiliate referrals, after Rewardful's and Dub's client scripts. A partner's tracked link (`<mrrocket>/r/<code>`)
|
|
3
|
+
* counts the click on MRRocket and sends the visitor to your site with `?mrr_ref=<code>&mrr_click=<clickId>`; a direct
|
|
4
|
+
* link is `https://your.site/?ref=<code>`. The provider reads either on page load, keeps the referral for the program's
|
|
5
|
+
* cookie days in a FIRST-PARTY cookie on your domain (`mrr_ref`) plus localStorage, and sends a click beacon for direct
|
|
6
|
+
* links. MRRocket never sets a cookie for you: its domain is not yours.
|
|
7
|
+
*
|
|
8
|
+
* Storage follows consent (see the README): with `attribution="consent"` (default) the referral is only stored when
|
|
9
|
+
* the provider's consent is "cookies"; otherwise it lives in memory for the page load. `attribution="always"` stores
|
|
10
|
+
* it whatever the consent (your legal call), `"off"` ignores referrals.
|
|
11
|
+
*
|
|
12
|
+
* Plain functions, no React: `getReferral()` and `referralForCheckout()` work in any client code, and on the server
|
|
13
|
+
* with the cookie's value (`readReferralCookie`).
|
|
14
|
+
*/
|
|
15
|
+
export interface Referral {
|
|
16
|
+
/** The partner's code. */
|
|
17
|
+
code: string;
|
|
18
|
+
/** The click MRRocket counted (tracked link or beacon); null when it was not counted. */
|
|
19
|
+
clickId: string | null;
|
|
20
|
+
/** When the visitor arrived through the partner, ms. */
|
|
21
|
+
at: number;
|
|
22
|
+
}
|
|
23
|
+
/** What to pass to Stripe Checkout (`stripe.checkout.sessions.create({ ..., ...referralForCheckout() })`). */
|
|
24
|
+
export interface CheckoutReferral {
|
|
25
|
+
client_reference_id: string;
|
|
26
|
+
metadata: {
|
|
27
|
+
mrr_ref: string;
|
|
28
|
+
mrr_click: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export type AttributionMode = "consent" | "always" | "off";
|
|
32
|
+
export declare const REFERRAL_COOKIE = "mrr_ref";
|
|
33
|
+
export declare const REFERRAL_STORAGE_KEY = "mrr_ref";
|
|
34
|
+
export declare const REF_PARAM = "mrr_ref";
|
|
35
|
+
export declare const CLICK_PARAM = "mrr_click";
|
|
36
|
+
export declare const DEFAULT_REFERRAL_DAYS = 60;
|
|
37
|
+
/** The referral in the landing URL: the tracked link's markers first, else the direct-link parameter (`ref`). */
|
|
38
|
+
export declare const readReferralParams: (search: string, param?: string) => {
|
|
39
|
+
code: string;
|
|
40
|
+
clickId: string | null;
|
|
41
|
+
direct: boolean;
|
|
42
|
+
} | null;
|
|
43
|
+
/** `code.clickId.at`: dots never occur in a code or a click id. */
|
|
44
|
+
export declare const encodeReferral: (r: Referral) => string;
|
|
45
|
+
/** A stored referral, or null when it is malformed or older than `maxDays`. */
|
|
46
|
+
export declare const decodeReferral: (value: string | null | undefined, maxDays?: number, now?: number) => Referral | null;
|
|
47
|
+
/** The referral for a Stripe Checkout Session: `client_reference_id` and `metadata`. Null without a referral. */
|
|
48
|
+
export declare const referralForCheckout: (r?: Referral | null) => CheckoutReferral | null;
|
|
49
|
+
/**
|
|
50
|
+
* A Stripe Payment Link (or Pricing Table URL) with the referral as `client_reference_id`. Payment Links only accept
|
|
51
|
+
* letters, digits, `-` and `_` there, so the form is `mrr_ref__<code>__<clickId>`. Without a referral the URL is unchanged.
|
|
52
|
+
*/
|
|
53
|
+
export declare const referralLink: (url: string, r?: Referral | null) => string;
|
|
54
|
+
/** The visitor's referral: this page load's, else the stored one. Null on the server (use `readReferralCookie` there). */
|
|
55
|
+
export declare const getReferral: () => Referral | null;
|
|
56
|
+
/** The referral from the `mrr_ref` cookie's value, for server code (a route that creates the Checkout Session). */
|
|
57
|
+
export declare const readReferralCookie: (value: string | null | undefined) => Referral | null;
|
|
58
|
+
/** Writes the referral to the first-party cookie and localStorage for `days`. */
|
|
59
|
+
export declare const storeReferral: (r: Referral, days: number) => void;
|
|
60
|
+
/** Deletes what was stored (consent withdrawn); the referral of this page load stays in memory. */
|
|
61
|
+
export declare const forgetStoredReferral: () => void;
|
|
62
|
+
/** For tests and `consent="off"`: forget the page load's referral too. */
|
|
63
|
+
export declare const resetReferral: () => void;
|
|
64
|
+
export interface CaptureOptions {
|
|
65
|
+
/** `location.search` of the landing page. */
|
|
66
|
+
search: string;
|
|
67
|
+
/** The direct-link parameter, default `ref`. */
|
|
68
|
+
param?: string;
|
|
69
|
+
/** May the referral be stored (consent)? */
|
|
70
|
+
store: boolean;
|
|
71
|
+
/** Days to keep it. */
|
|
72
|
+
days: number;
|
|
73
|
+
/** Sends the click beacon; answers the click id MRRocket counted (or confirmed). */
|
|
74
|
+
beacon?: (body: {
|
|
75
|
+
code: string;
|
|
76
|
+
clickId: string | null;
|
|
77
|
+
}) => Promise<string | null>;
|
|
78
|
+
now?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reads the referral of a page load: a new one from the URL (sent to the beacon and stored when allowed) or the stored
|
|
82
|
+
* one. A new link replaces an older referral (last click within the window wins). Returns the referral in effect.
|
|
83
|
+
*/
|
|
84
|
+
export declare const captureReferral: (o: CaptureOptions) => Promise<Referral | null>;
|
|
85
|
+
/** Events that carry the referral: identify, signup and your own events (not page views or exposures). */
|
|
86
|
+
export declare const carriesReferral: (event: string) => boolean;
|
package/dist/referral.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/referral.ts
|
|
2
|
+
var REFERRAL_COOKIE = "mrr_ref";
|
|
3
|
+
var REFERRAL_STORAGE_KEY = "mrr_ref";
|
|
4
|
+
var REF_PARAM = "mrr_ref";
|
|
5
|
+
var CLICK_PARAM = "mrr_click";
|
|
6
|
+
var DEFAULT_REFERRAL_DAYS = 60;
|
|
7
|
+
var CODE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
8
|
+
var DAY_MS = 864e5;
|
|
9
|
+
var readReferralParams = (search, param = "ref") => {
|
|
10
|
+
const q = new URLSearchParams(search);
|
|
11
|
+
const marked = q.get(REF_PARAM);
|
|
12
|
+
if (marked && CODE.test(marked)) {
|
|
13
|
+
const click = q.get(CLICK_PARAM);
|
|
14
|
+
return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };
|
|
15
|
+
}
|
|
16
|
+
const direct = q.get(param);
|
|
17
|
+
return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;
|
|
18
|
+
};
|
|
19
|
+
var encodeReferral = (r) => `${r.code}.${r.clickId ?? ""}.${r.at}`;
|
|
20
|
+
var decodeReferral = (value, maxDays = DEFAULT_REFERRAL_DAYS, now = Date.now()) => {
|
|
21
|
+
if (!value) return null;
|
|
22
|
+
const [code, click, at] = value.split(".");
|
|
23
|
+
const t = Number(at);
|
|
24
|
+
if (!code || !CODE.test(code) || !Number.isFinite(t) || click && !CODE.test(click)) return null;
|
|
25
|
+
if (now - t > maxDays * DAY_MS || t > now + DAY_MS) return null;
|
|
26
|
+
return { code, clickId: click || null, at: t };
|
|
27
|
+
};
|
|
28
|
+
var referralForCheckout = (r = getReferral()) => r ? { client_reference_id: `mrr_ref:${r.code}:${r.clickId ?? ""}`, metadata: { mrr_ref: r.code, mrr_click: r.clickId ?? "" } } : null;
|
|
29
|
+
var referralLink = (url, r = getReferral()) => {
|
|
30
|
+
if (!r) return url;
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(url);
|
|
33
|
+
u.searchParams.set("client_reference_id", `mrr_ref__${r.code}__${r.clickId ?? ""}`);
|
|
34
|
+
return u.toString();
|
|
35
|
+
} catch {
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var current = null;
|
|
40
|
+
var hasDocument = () => typeof document !== "undefined";
|
|
41
|
+
var readCookie = (name) => {
|
|
42
|
+
if (!hasDocument()) return void 0;
|
|
43
|
+
const raw = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
44
|
+
try {
|
|
45
|
+
return raw ? decodeURIComponent(raw) : void 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var storage = () => {
|
|
51
|
+
try {
|
|
52
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var getReferral = () => {
|
|
58
|
+
if (current) return current;
|
|
59
|
+
if (!hasDocument()) return null;
|
|
60
|
+
return decodeReferral(readCookie(REFERRAL_COOKIE), 3650) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), 3650);
|
|
61
|
+
};
|
|
62
|
+
var readReferralCookie = (value) => decodeReferral(value, 3650);
|
|
63
|
+
var storeReferral = (r, days) => {
|
|
64
|
+
if (!hasDocument()) return;
|
|
65
|
+
const maxAge = Math.max(1, Math.round(days)) * 86400;
|
|
66
|
+
const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; secure" : "";
|
|
67
|
+
document.cookie = `${REFERRAL_COOKIE}=${encodeURIComponent(encodeReferral(r))}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
|
|
68
|
+
try {
|
|
69
|
+
storage()?.setItem(REFERRAL_STORAGE_KEY, encodeReferral(r));
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var forgetStoredReferral = () => {
|
|
74
|
+
if (!hasDocument()) return;
|
|
75
|
+
document.cookie = `${REFERRAL_COOKIE}=; path=/; max-age=0; samesite=lax`;
|
|
76
|
+
try {
|
|
77
|
+
storage()?.removeItem(REFERRAL_STORAGE_KEY);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var resetReferral = () => {
|
|
82
|
+
current = null;
|
|
83
|
+
};
|
|
84
|
+
var captureReferral = async (o) => {
|
|
85
|
+
const found = readReferralParams(o.search, o.param);
|
|
86
|
+
const now = o.now ?? Date.now();
|
|
87
|
+
if (!found) {
|
|
88
|
+
if (!current && o.store) {
|
|
89
|
+
current = decodeReferral(readCookie(REFERRAL_COOKIE), o.days, now) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), o.days, now);
|
|
90
|
+
if (!current) forgetStoredReferral();
|
|
91
|
+
}
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
current = { code: found.code, clickId: found.clickId, at: now };
|
|
95
|
+
if (o.store) storeReferral(current, o.days);
|
|
96
|
+
if (o.beacon) {
|
|
97
|
+
try {
|
|
98
|
+
const id = await o.beacon({ code: found.code, clickId: found.clickId });
|
|
99
|
+
if (id && current && current.code === found.code && !current.clickId) {
|
|
100
|
+
current = { ...current, clickId: id };
|
|
101
|
+
if (o.store) storeReferral(current, o.days);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
};
|
|
108
|
+
var carriesReferral = (event) => event === "$identify" || !event.startsWith("$");
|
|
109
|
+
export {
|
|
110
|
+
CLICK_PARAM,
|
|
111
|
+
DEFAULT_REFERRAL_DAYS,
|
|
112
|
+
REFERRAL_COOKIE,
|
|
113
|
+
REFERRAL_STORAGE_KEY,
|
|
114
|
+
REF_PARAM,
|
|
115
|
+
captureReferral,
|
|
116
|
+
carriesReferral,
|
|
117
|
+
decodeReferral,
|
|
118
|
+
encodeReferral,
|
|
119
|
+
forgetStoredReferral,
|
|
120
|
+
getReferral,
|
|
121
|
+
readReferralCookie,
|
|
122
|
+
readReferralParams,
|
|
123
|
+
referralForCheckout,
|
|
124
|
+
referralLink,
|
|
125
|
+
resetReferral,
|
|
126
|
+
storeReferral
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=referral.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/referral.ts"],"sourcesContent":["/**\n * Affiliate referrals, after Rewardful's and Dub's client scripts. A partner's tracked link (`<mrrocket>/r/<code>`)\n * counts the click on MRRocket and sends the visitor to your site with `?mrr_ref=<code>&mrr_click=<clickId>`; a direct\n * link is `https://your.site/?ref=<code>`. The provider reads either on page load, keeps the referral for the program's\n * cookie days in a FIRST-PARTY cookie on your domain (`mrr_ref`) plus localStorage, and sends a click beacon for direct\n * links. MRRocket never sets a cookie for you: its domain is not yours.\n *\n * Storage follows consent (see the README): with `attribution=\"consent\"` (default) the referral is only stored when\n * the provider's consent is \"cookies\"; otherwise it lives in memory for the page load. `attribution=\"always\"` stores\n * it whatever the consent (your legal call), `\"off\"` ignores referrals.\n *\n * Plain functions, no React: `getReferral()` and `referralForCheckout()` work in any client code, and on the server\n * with the cookie's value (`readReferralCookie`).\n */\n\nexport interface Referral {\n /** The partner's code. */\n code: string;\n /** The click MRRocket counted (tracked link or beacon); null when it was not counted. */\n clickId: string | null;\n /** When the visitor arrived through the partner, ms. */\n at: number;\n}\n\n/** What to pass to Stripe Checkout (`stripe.checkout.sessions.create({ ..., ...referralForCheckout() })`). */\nexport interface CheckoutReferral {\n client_reference_id: string;\n metadata: { mrr_ref: string; mrr_click: string };\n}\n\nexport type AttributionMode = \"consent\" | \"always\" | \"off\";\n\nexport const REFERRAL_COOKIE = \"mrr_ref\";\nexport const REFERRAL_STORAGE_KEY = \"mrr_ref\";\nexport const REF_PARAM = \"mrr_ref\";\nexport const CLICK_PARAM = \"mrr_click\";\nexport const DEFAULT_REFERRAL_DAYS = 60;\n\nconst CODE = /^[A-Za-z0-9_-]{1,64}$/;\nconst DAY_MS = 86_400_000;\n\n/** The referral in the landing URL: the tracked link's markers first, else the direct-link parameter (`ref`). */\nexport const readReferralParams = (search: string, param = \"ref\"): { code: string; clickId: string | null; direct: boolean } | null => {\n const q = new URLSearchParams(search);\n const marked = q.get(REF_PARAM);\n if (marked && CODE.test(marked)) {\n const click = q.get(CLICK_PARAM);\n return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };\n }\n const direct = q.get(param);\n return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;\n};\n\n/** `code.clickId.at`: dots never occur in a code or a click id. */\nexport const encodeReferral = (r: Referral) => `${r.code}.${r.clickId ?? \"\"}.${r.at}`;\n\n/** A stored referral, or null when it is malformed or older than `maxDays`. */\nexport const decodeReferral = (value: string | null | undefined, maxDays = DEFAULT_REFERRAL_DAYS, now = Date.now()): Referral | null => {\n if (!value) return null;\n const [code, click, at] = value.split(\".\");\n const t = Number(at);\n if (!code || !CODE.test(code) || !Number.isFinite(t) || (click && !CODE.test(click))) return null;\n if (now - t > maxDays * DAY_MS || t > now + DAY_MS) return null;\n return { code, clickId: click || null, at: t };\n};\n\n/** The referral for a Stripe Checkout Session: `client_reference_id` and `metadata`. Null without a referral. */\nexport const referralForCheckout = (r: Referral | null = getReferral()): CheckoutReferral | null =>\n r ? { client_reference_id: `mrr_ref:${r.code}:${r.clickId ?? \"\"}`, metadata: { mrr_ref: r.code, mrr_click: r.clickId ?? \"\" } } : null;\n\n/**\n * A Stripe Payment Link (or Pricing Table URL) with the referral as `client_reference_id`. Payment Links only accept\n * letters, digits, `-` and `_` there, so the form is `mrr_ref__<code>__<clickId>`. Without a referral the URL is unchanged.\n */\nexport const referralLink = (url: string, r: Referral | null = getReferral()) => {\n if (!r) return url;\n try {\n const u = new URL(url);\n u.searchParams.set(\"client_reference_id\", `mrr_ref__${r.code}__${r.clickId ?? \"\"}`);\n return u.toString();\n } catch {\n return url;\n }\n};\n\n// ---------- The browser side ----------\n\nlet current: Referral | null = null;\n\nconst hasDocument = () => typeof document !== \"undefined\";\n\nconst readCookie = (name: string) => {\n if (!hasDocument()) return undefined;\n const raw = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`))\n ?.slice(name.length + 1);\n try {\n return raw ? decodeURIComponent(raw) : undefined;\n } catch {\n return raw;\n }\n};\n\nconst storage = () => {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n};\n\n/** The visitor's referral: this page load's, else the stored one. Null on the server (use `readReferralCookie` there). */\nexport const getReferral = (): Referral | null => {\n if (current) return current;\n if (!hasDocument()) return null;\n return decodeReferral(readCookie(REFERRAL_COOKIE), 3650) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), 3650);\n};\n\n/** The referral from the `mrr_ref` cookie's value, for server code (a route that creates the Checkout Session). */\nexport const readReferralCookie = (value: string | null | undefined) => decodeReferral(value, 3650);\n\n/** Writes the referral to the first-party cookie and localStorage for `days`. */\nexport const storeReferral = (r: Referral, days: number) => {\n if (!hasDocument()) return;\n const maxAge = Math.max(1, Math.round(days)) * 86_400;\n const secure = typeof location !== \"undefined\" && location.protocol === \"https:\" ? \"; secure\" : \"\";\n document.cookie = `${REFERRAL_COOKIE}=${encodeURIComponent(encodeReferral(r))}; path=/; max-age=${maxAge}; samesite=lax${secure}`;\n try {\n storage()?.setItem(REFERRAL_STORAGE_KEY, encodeReferral(r));\n } catch {\n // Storage full or blocked: the cookie is enough.\n }\n};\n\n/** Deletes what was stored (consent withdrawn); the referral of this page load stays in memory. */\nexport const forgetStoredReferral = () => {\n if (!hasDocument()) return;\n document.cookie = `${REFERRAL_COOKIE}=; path=/; max-age=0; samesite=lax`;\n try {\n storage()?.removeItem(REFERRAL_STORAGE_KEY);\n } catch {\n // Nothing stored.\n }\n};\n\n/** For tests and `consent=\"off\"`: forget the page load's referral too. */\nexport const resetReferral = () => {\n current = null;\n};\n\nexport interface CaptureOptions {\n /** `location.search` of the landing page. */\n search: string;\n /** The direct-link parameter, default `ref`. */\n param?: string;\n /** May the referral be stored (consent)? */\n store: boolean;\n /** Days to keep it. */\n days: number;\n /** Sends the click beacon; answers the click id MRRocket counted (or confirmed). */\n beacon?: (body: { code: string; clickId: string | null }) => Promise<string | null>;\n now?: number;\n}\n\n/**\n * Reads the referral of a page load: a new one from the URL (sent to the beacon and stored when allowed) or the stored\n * one. A new link replaces an older referral (last click within the window wins). Returns the referral in effect.\n */\nexport const captureReferral = async (o: CaptureOptions): Promise<Referral | null> => {\n const found = readReferralParams(o.search, o.param);\n const now = o.now ?? Date.now();\n if (!found) {\n if (!current && o.store) {\n current = decodeReferral(readCookie(REFERRAL_COOKIE), o.days, now) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), o.days, now);\n if (!current) forgetStoredReferral();\n }\n return current;\n }\n current = { code: found.code, clickId: found.clickId, at: now };\n if (o.store) storeReferral(current, o.days);\n if (o.beacon) {\n try {\n const id = await o.beacon({ code: found.code, clickId: found.clickId });\n if (id && current && current.code === found.code && !current.clickId) {\n current = { ...current, clickId: id };\n if (o.store) storeReferral(current, o.days);\n }\n } catch {\n // The referral still counts for checkout without a click id.\n }\n }\n return current;\n};\n\n/** Events that carry the referral: identify, signup and your own events (not page views or exposures). */\nexport const carriesReferral = (event: string) => event === \"$identify\" || !event.startsWith(\"$\");\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export declare const DEFAULT_SAMPLE_RATE = 0.1;
|
|
2
|
+
/** A recording stops after 30 minutes. */
|
|
3
|
+
export declare const MAX_RECORDING_MS: number;
|
|
4
|
+
/** A visit outside the sample keeps about this much in memory, from the last full snapshot on. */
|
|
5
|
+
export declare const BUFFER_MS: number;
|
|
6
|
+
/** How often a buffering visit takes a new full snapshot, so the buffer can drop what is older. */
|
|
7
|
+
export declare const CHECKOUT_MS: number;
|
|
8
|
+
/** Upload a chunk every 5 seconds or once it holds about 64 KB of events, whichever comes first. */
|
|
9
|
+
export declare const FLUSH_MS: number;
|
|
10
|
+
export declare const FLUSH_BYTES: number;
|
|
11
|
+
/** The buffer never holds more than this; the oldest snapshot goes first. */
|
|
12
|
+
export declare const BUFFER_MAX_BYTES: number;
|
|
13
|
+
/** The server takes at most 1 MB per request; a chunk that would not fit ends the recording. */
|
|
14
|
+
export declare const MAX_REQUEST_BYTES: number;
|
|
15
|
+
/** Why a recording was uploaded. */
|
|
16
|
+
export type ReplayTrigger = "sample" | "error" | "rage" | "purchase" | "checkout";
|
|
17
|
+
/** Never recorded: password, hidden and card fields, payment iframes, and whatever the site marks `data-ck-block`. */
|
|
18
|
+
export declare const BLOCK_SELECTOR: string;
|
|
19
|
+
export declare const MASK_TEXT_SELECTOR = "[data-ck-mask]";
|
|
20
|
+
/** Whether this session uploads from its start. Stable per session id, so every page of a session agrees. */
|
|
21
|
+
export declare const inSample: (sessionId: string, rate: number) => boolean;
|
|
22
|
+
/** A tracked event that uploads the buffered minute: names starting with `purchase` or `checkout`. */
|
|
23
|
+
export declare const triggerOfEvent: (event: string) => "purchase" | "checkout" | null;
|
|
24
|
+
/** Typed text never reaches MRRocket: every input value becomes stars, at most 12. */
|
|
25
|
+
export declare const maskInput: (text: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* rrweb's record options: all inputs masked, blocked fields left out, text in `data-ck-mask` masked, no canvas, video,
|
|
28
|
+
* fonts or inlined images, scripts and head clutter dropped. `emit` receives the events.
|
|
29
|
+
*/
|
|
30
|
+
export declare const recordOptions: <E>(emit: (e: E) => void) => {
|
|
31
|
+
emit: (e: E) => void;
|
|
32
|
+
maskAllInputs: boolean;
|
|
33
|
+
maskInputOptions: {
|
|
34
|
+
color: boolean;
|
|
35
|
+
date: boolean;
|
|
36
|
+
"datetime-local": boolean;
|
|
37
|
+
email: boolean;
|
|
38
|
+
month: boolean;
|
|
39
|
+
number: boolean;
|
|
40
|
+
range: boolean;
|
|
41
|
+
search: boolean;
|
|
42
|
+
tel: boolean;
|
|
43
|
+
text: boolean;
|
|
44
|
+
time: boolean;
|
|
45
|
+
url: boolean;
|
|
46
|
+
week: boolean;
|
|
47
|
+
textarea: boolean;
|
|
48
|
+
select: boolean;
|
|
49
|
+
password: boolean;
|
|
50
|
+
};
|
|
51
|
+
maskInputFn: (text: string) => string;
|
|
52
|
+
maskTextSelector: string;
|
|
53
|
+
blockSelector: string;
|
|
54
|
+
recordCanvas: boolean;
|
|
55
|
+
recordCrossOriginIframes: boolean;
|
|
56
|
+
collectFonts: boolean;
|
|
57
|
+
inlineImages: boolean;
|
|
58
|
+
slimDOMOptions: "all";
|
|
59
|
+
sampling: {
|
|
60
|
+
mousemove: number;
|
|
61
|
+
scroll: number;
|
|
62
|
+
media: number;
|
|
63
|
+
input: "last";
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** The few rrweb fields this module reads. Type 4 is the Meta event that starts each full snapshot. */
|
|
67
|
+
export interface ReplayEvent {
|
|
68
|
+
type: number;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The last minute of a visit that is not uploaded (yet). Events are kept in segments that each start at a full
|
|
73
|
+
* snapshot; a segment is dropped once the next one already covers the whole window, so whatever is drained starts
|
|
74
|
+
* with a full snapshot and holds at least the last `windowMs`.
|
|
75
|
+
*/
|
|
76
|
+
export declare class RingBuffer<E extends ReplayEvent = ReplayEvent> {
|
|
77
|
+
private readonly windowMs;
|
|
78
|
+
private readonly maxBytes;
|
|
79
|
+
private segments;
|
|
80
|
+
constructor(windowMs?: number, maxBytes?: number);
|
|
81
|
+
push(e: E, bytes: number): void;
|
|
82
|
+
get bytes(): number;
|
|
83
|
+
get size(): number;
|
|
84
|
+
/** The oldest event kept, or null when empty. */
|
|
85
|
+
get first(): number;
|
|
86
|
+
/** Everything kept, oldest first, and empties the buffer. */
|
|
87
|
+
drain(): E[];
|
|
88
|
+
clear(): void;
|
|
89
|
+
}
|
|
90
|
+
/** Whether a chunk is due: enough bytes, or enough time since the last upload. */
|
|
91
|
+
export declare const chunkDue: (bytes: number, sinceFlushMs: number) => boolean;
|
|
92
|
+
/** Base64 of bytes, in slices so a large chunk does not overflow the call stack. */
|
|
93
|
+
export declare const toBase64: (bytes: Uint8Array) => string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/replay-core.ts
|
|
2
|
+
import { BLOCK_ATTR_SELECTOR, MASK_SELECTOR } from "./capture.js";
|
|
3
|
+
import { hashUnit } from "./core.js";
|
|
4
|
+
var DEFAULT_SAMPLE_RATE = 0.1;
|
|
5
|
+
var MAX_RECORDING_MS = 30 * 60 * 1e3;
|
|
6
|
+
var BUFFER_MS = 60 * 1e3;
|
|
7
|
+
var CHECKOUT_MS = 30 * 1e3;
|
|
8
|
+
var FLUSH_MS = 5 * 1e3;
|
|
9
|
+
var FLUSH_BYTES = 64 * 1024;
|
|
10
|
+
var BUFFER_MAX_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
var MAX_REQUEST_BYTES = 1024 * 1024;
|
|
12
|
+
var BLOCK_SELECTOR = [
|
|
13
|
+
BLOCK_ATTR_SELECTOR,
|
|
14
|
+
'input[type="password"]',
|
|
15
|
+
'input[type="hidden"]',
|
|
16
|
+
'[autocomplete^="cc-"]',
|
|
17
|
+
'input[name*="card" i]',
|
|
18
|
+
'input[name*="cvc" i]',
|
|
19
|
+
'input[name*="cvv" i]',
|
|
20
|
+
'iframe[src*="js.stripe.com"]',
|
|
21
|
+
'iframe[src*="paypal.com"]'
|
|
22
|
+
].join(",");
|
|
23
|
+
var MASK_TEXT_SELECTOR = MASK_SELECTOR;
|
|
24
|
+
var inSample = (sessionId, rate) => hashUnit(`replay:${sessionId}`) < Math.min(1, Math.max(0, rate));
|
|
25
|
+
var triggerOfEvent = (event) => {
|
|
26
|
+
const e = event.toLowerCase().replace(/^\$/, "");
|
|
27
|
+
return e.startsWith("purchase") ? "purchase" : e.startsWith("checkout") ? "checkout" : null;
|
|
28
|
+
};
|
|
29
|
+
var maskInput = (text) => "*".repeat(Math.min(12, text.length));
|
|
30
|
+
var recordOptions = (emit) => ({
|
|
31
|
+
emit,
|
|
32
|
+
maskAllInputs: true,
|
|
33
|
+
maskInputOptions: {
|
|
34
|
+
color: true,
|
|
35
|
+
date: true,
|
|
36
|
+
"datetime-local": true,
|
|
37
|
+
email: true,
|
|
38
|
+
month: true,
|
|
39
|
+
number: true,
|
|
40
|
+
range: true,
|
|
41
|
+
search: true,
|
|
42
|
+
tel: true,
|
|
43
|
+
text: true,
|
|
44
|
+
time: true,
|
|
45
|
+
url: true,
|
|
46
|
+
week: true,
|
|
47
|
+
textarea: true,
|
|
48
|
+
select: true,
|
|
49
|
+
password: true
|
|
50
|
+
},
|
|
51
|
+
maskInputFn: maskInput,
|
|
52
|
+
maskTextSelector: MASK_TEXT_SELECTOR,
|
|
53
|
+
blockSelector: BLOCK_SELECTOR,
|
|
54
|
+
recordCanvas: false,
|
|
55
|
+
recordCrossOriginIframes: false,
|
|
56
|
+
collectFonts: false,
|
|
57
|
+
inlineImages: false,
|
|
58
|
+
slimDOMOptions: "all",
|
|
59
|
+
sampling: { mousemove: 50, scroll: 150, media: 800, input: "last" }
|
|
60
|
+
});
|
|
61
|
+
var RingBuffer = class {
|
|
62
|
+
constructor(windowMs = BUFFER_MS, maxBytes = BUFFER_MAX_BYTES) {
|
|
63
|
+
this.windowMs = windowMs;
|
|
64
|
+
this.maxBytes = maxBytes;
|
|
65
|
+
}
|
|
66
|
+
windowMs;
|
|
67
|
+
maxBytes;
|
|
68
|
+
segments = [];
|
|
69
|
+
push(e, bytes) {
|
|
70
|
+
if (e.type === 4 || !this.segments.length) this.segments.push({ events: [], bytes: 0 });
|
|
71
|
+
const seg = this.segments[this.segments.length - 1];
|
|
72
|
+
seg.events.push(e);
|
|
73
|
+
seg.bytes += bytes;
|
|
74
|
+
const from = e.timestamp - this.windowMs;
|
|
75
|
+
while (this.segments.length > 1 && (this.segments[1].events[0].timestamp <= from || this.bytes > this.maxBytes)) this.segments.shift();
|
|
76
|
+
}
|
|
77
|
+
get bytes() {
|
|
78
|
+
return this.segments.reduce((s, x) => s + x.bytes, 0);
|
|
79
|
+
}
|
|
80
|
+
get size() {
|
|
81
|
+
return this.segments.reduce((s, x) => s + x.events.length, 0);
|
|
82
|
+
}
|
|
83
|
+
/** The oldest event kept, or null when empty. */
|
|
84
|
+
get first() {
|
|
85
|
+
return this.segments[0]?.events[0]?.timestamp ?? null;
|
|
86
|
+
}
|
|
87
|
+
/** Everything kept, oldest first, and empties the buffer. */
|
|
88
|
+
drain() {
|
|
89
|
+
const out = this.segments.flatMap((s) => s.events);
|
|
90
|
+
this.segments = [];
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
clear() {
|
|
94
|
+
this.segments = [];
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var chunkDue = (bytes, sinceFlushMs) => bytes >= FLUSH_BYTES || bytes > 0 && sinceFlushMs >= FLUSH_MS;
|
|
98
|
+
var toBase64 = (bytes) => {
|
|
99
|
+
let s = "";
|
|
100
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
101
|
+
return btoa(s);
|
|
102
|
+
};
|
|
103
|
+
export {
|
|
104
|
+
BLOCK_SELECTOR,
|
|
105
|
+
BUFFER_MAX_BYTES,
|
|
106
|
+
BUFFER_MS,
|
|
107
|
+
CHECKOUT_MS,
|
|
108
|
+
DEFAULT_SAMPLE_RATE,
|
|
109
|
+
FLUSH_BYTES,
|
|
110
|
+
FLUSH_MS,
|
|
111
|
+
MASK_TEXT_SELECTOR,
|
|
112
|
+
MAX_RECORDING_MS,
|
|
113
|
+
MAX_REQUEST_BYTES,
|
|
114
|
+
RingBuffer,
|
|
115
|
+
chunkDue,
|
|
116
|
+
inSample,
|
|
117
|
+
maskInput,
|
|
118
|
+
recordOptions,
|
|
119
|
+
toBase64,
|
|
120
|
+
triggerOfEvent
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=replay-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/replay-core.ts"],"sourcesContent":["/**\n * The rules of session recording, without the browser: which visits upload from the start, what a trigger is, how the\n * last minute is kept in memory, how events become upload chunks, and the privacy defaults handed to rrweb. The\n * recorder itself (./replay.ts) is loaded only when a visit records; this file is shared with MRRocket's own checks.\n */\nimport { BLOCK_ATTR_SELECTOR, MASK_SELECTOR } from \"./capture\";\nimport { hashUnit } from \"./core\";\n\nexport const DEFAULT_SAMPLE_RATE = 0.1;\n/** A recording stops after 30 minutes. */\nexport const MAX_RECORDING_MS = 30 * 60 * 1000;\n/** A visit outside the sample keeps about this much in memory, from the last full snapshot on. */\nexport const BUFFER_MS = 60 * 1000;\n/** How often a buffering visit takes a new full snapshot, so the buffer can drop what is older. */\nexport const CHECKOUT_MS = 30 * 1000;\n/** Upload a chunk every 5 seconds or once it holds about 64 KB of events, whichever comes first. */\nexport const FLUSH_MS = 5 * 1000;\nexport const FLUSH_BYTES = 64 * 1024;\n/** The buffer never holds more than this; the oldest snapshot goes first. */\nexport const BUFFER_MAX_BYTES = 4 * 1024 * 1024;\n/** The server takes at most 1 MB per request; a chunk that would not fit ends the recording. */\nexport const MAX_REQUEST_BYTES = 1024 * 1024;\n\n/** Why a recording was uploaded. */\nexport type ReplayTrigger = \"sample\" | \"error\" | \"rage\" | \"purchase\" | \"checkout\";\n\n/** Never recorded: password, hidden and card fields, payment iframes, and whatever the site marks `data-ck-block`. */\nexport const BLOCK_SELECTOR = [\n BLOCK_ATTR_SELECTOR,\n 'input[type=\"password\"]',\n 'input[type=\"hidden\"]',\n '[autocomplete^=\"cc-\"]',\n 'input[name*=\"card\" i]',\n 'input[name*=\"cvc\" i]',\n 'input[name*=\"cvv\" i]',\n 'iframe[src*=\"js.stripe.com\"]',\n 'iframe[src*=\"paypal.com\"]',\n].join(\",\");\nexport const MASK_TEXT_SELECTOR = MASK_SELECTOR;\n\n/** Whether this session uploads from its start. Stable per session id, so every page of a session agrees. */\nexport const inSample = (sessionId: string, rate: number) => hashUnit(`replay:${sessionId}`) < Math.min(1, Math.max(0, rate));\n\n/** A tracked event that uploads the buffered minute: names starting with `purchase` or `checkout`. */\nexport const triggerOfEvent = (event: string): \"purchase\" | \"checkout\" | null => {\n const e = event.toLowerCase().replace(/^\\$/, \"\");\n return e.startsWith(\"purchase\") ? \"purchase\" : e.startsWith(\"checkout\") ? \"checkout\" : null;\n};\n\n/** Typed text never reaches MRRocket: every input value becomes stars, at most 12. */\nexport const maskInput = (text: string) => \"*\".repeat(Math.min(12, text.length));\n\n/**\n * rrweb's record options: all inputs masked, blocked fields left out, text in `data-ck-mask` masked, no canvas, video,\n * fonts or inlined images, scripts and head clutter dropped. `emit` receives the events.\n */\nexport const recordOptions = <E>(emit: (e: E) => void) => ({\n emit,\n maskAllInputs: true,\n maskInputOptions: {\n color: true,\n date: true,\n \"datetime-local\": true,\n email: true,\n month: true,\n number: true,\n range: true,\n search: true,\n tel: true,\n text: true,\n time: true,\n url: true,\n week: true,\n textarea: true,\n select: true,\n password: true,\n },\n maskInputFn: maskInput,\n maskTextSelector: MASK_TEXT_SELECTOR,\n blockSelector: BLOCK_SELECTOR,\n recordCanvas: false,\n recordCrossOriginIframes: false,\n collectFonts: false,\n inlineImages: false,\n slimDOMOptions: \"all\" as const,\n sampling: { mousemove: 50, scroll: 150, media: 800, input: \"last\" as const },\n});\n\n/** The few rrweb fields this module reads. Type 4 is the Meta event that starts each full snapshot. */\nexport interface ReplayEvent {\n type: number;\n timestamp: number;\n}\n\n/**\n * The last minute of a visit that is not uploaded (yet). Events are kept in segments that each start at a full\n * snapshot; a segment is dropped once the next one already covers the whole window, so whatever is drained starts\n * with a full snapshot and holds at least the last `windowMs`.\n */\nexport class RingBuffer<E extends ReplayEvent = ReplayEvent> {\n private segments: { events: E[]; bytes: number }[] = [];\n constructor(\n private readonly windowMs = BUFFER_MS,\n private readonly maxBytes = BUFFER_MAX_BYTES,\n ) {}\n\n push(e: E, bytes: number) {\n if (e.type === 4 || !this.segments.length) this.segments.push({ events: [], bytes: 0 });\n const seg = this.segments[this.segments.length - 1];\n seg.events.push(e);\n seg.bytes += bytes;\n const from = e.timestamp - this.windowMs;\n while (this.segments.length > 1 && (this.segments[1].events[0].timestamp <= from || this.bytes > this.maxBytes)) this.segments.shift();\n }\n\n get bytes() {\n return this.segments.reduce((s, x) => s + x.bytes, 0);\n }\n\n get size() {\n return this.segments.reduce((s, x) => s + x.events.length, 0);\n }\n\n /** The oldest event kept, or null when empty. */\n get first() {\n return this.segments[0]?.events[0]?.timestamp ?? null;\n }\n\n /** Everything kept, oldest first, and empties the buffer. */\n drain(): E[] {\n const out = this.segments.flatMap((s) => s.events);\n this.segments = [];\n return out;\n }\n\n clear() {\n this.segments = [];\n }\n}\n\n/** Whether a chunk is due: enough bytes, or enough time since the last upload. */\nexport const chunkDue = (bytes: number, sinceFlushMs: number) => bytes >= FLUSH_BYTES || (bytes > 0 && sinceFlushMs >= FLUSH_MS);\n\n/** Base64 of bytes, in slices so a large chunk does not overflow the call stack. */\nexport const toBase64 = (bytes: Uint8Array) => {\n let s = \"\";\n for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));\n return btoa(s);\n};\n"],"mappings":"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","names":[]}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session recorder. The provider imports this file (and rrweb's recorder with it) only when a visit records: the
|
|
3
|
+
* product has recordings on and the visitor gave replay consent. Pages without recording never load it.
|
|
4
|
+
*
|
|
5
|
+
* A visit in the product's sample uploads from its start. Any other visit keeps the last minute in memory and uploads
|
|
6
|
+
* it only when something happens worth watching: a JS error, a rage click, or a `purchase…` / `checkout…` event; from
|
|
7
|
+
* then on it uploads like a sampled one. Chunks go every 5 seconds or 64 KB, gzip'd where the browser can, to
|
|
8
|
+
* `POST {host}/api/sdk/replay`; the last one leaves with sendBeacon when the page hides. A recording ends after 30
|
|
9
|
+
* minutes. Withdrawing consent stops it and drops what was not sent.
|
|
10
|
+
*/
|
|
11
|
+
import type { ClickInfo } from "./capture.js";
|
|
12
|
+
export interface ReplayOptions {
|
|
13
|
+
host: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** The SDK's session id; every chunk carries it, so the recording joins the session's events. */
|
|
16
|
+
sessionId: string;
|
|
17
|
+
/** The identify() id or the cookie id; missing cookieless until identify(). */
|
|
18
|
+
distinctId?: string;
|
|
19
|
+
/** Whether `distinctId` comes from identify() (a person) rather than the visitor cookie. */
|
|
20
|
+
identified?: boolean;
|
|
21
|
+
sampleRate: number;
|
|
22
|
+
warn: (message: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface ReplayHandle {
|
|
25
|
+
/** A tracked event; `purchase…` and `checkout…` upload the buffered minute. */
|
|
26
|
+
event(name: string): void;
|
|
27
|
+
click(c: ClickInfo, rage: boolean): void;
|
|
28
|
+
/** A client-side navigation. */
|
|
29
|
+
page(path: string): void;
|
|
30
|
+
identify(id: string): void;
|
|
31
|
+
/** Stops recording. `discard` (consent withdrawn) drops everything not sent yet. */
|
|
32
|
+
stop(discard?: boolean): void;
|
|
33
|
+
}
|
|
34
|
+
export declare const startReplay: (o: ReplayOptions) => Promise<ReplayHandle | null>;
|