@mrrocket/sdk 0.2.0 → 0.4.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 +16 -0
- package/README.md +142 -2
- package/dist/batch.d.ts +33 -0
- package/dist/batch.js +50 -0
- package/dist/batch.js.map +1 -0
- package/dist/browser/mrrocket-replay.js +78 -0
- package/dist/browser/mrrocket.js +1 -0
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +3 -1
- package/dist/browser.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/client.js +44 -83
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +94 -1
- package/dist/core.js +88 -4
- package/dist/core.js.map +1 -1
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +83 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/react-native/index.d.cts +259 -0
- package/dist/react-native/index.d.ts +259 -0
- package/dist/react-native/index.js +922 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/react-native/package.json +3 -0
- package/dist/referral.d.ts +6 -0
- package/dist/referral.js +7 -3
- package/dist/referral.js.map +1 -1
- package/dist/server.d.ts +13 -1
- package/dist/server.js +8 -1
- package/dist/server.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +2 -2
- package/dist/styles.js.map +1 -1
- package/dist/web-state.d.ts +36 -0
- package/dist/web-state.js +64 -0
- package/dist/web-state.js.map +1 -0
- package/package.json +56 -6
- package/react-native/package.json +7 -0
package/dist/core.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* No React, no browser APIs, so the server helper, the proxy helper and the client share it.
|
|
4
4
|
*/
|
|
5
5
|
export type VariantId = string;
|
|
6
|
+
/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */
|
|
7
|
+
export declare const SDK_VERSION = "0.4.0";
|
|
6
8
|
/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
|
|
7
9
|
export interface VariantConfig {
|
|
8
10
|
id: VariantId;
|
|
@@ -29,6 +31,66 @@ export interface CockpitConfig {
|
|
|
29
31
|
};
|
|
30
32
|
/** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */
|
|
31
33
|
replay?: ReplayConfig;
|
|
34
|
+
/** Feature flags of the product in Production (MRRocket › Build › Feature flags), evaluated by `useFlag` and `getFlag`. */
|
|
35
|
+
flags?: FlagConfig[];
|
|
36
|
+
}
|
|
37
|
+
/** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */
|
|
38
|
+
export interface FlagCondition {
|
|
39
|
+
/** "Email", "Plan", "Country", "id", "path" … Matched against the person's properties without regard to case. */
|
|
40
|
+
property: string;
|
|
41
|
+
op: "is" | "is not" | "in" | "not in" | "≥" | "in cohort";
|
|
42
|
+
values: string[];
|
|
43
|
+
}
|
|
44
|
+
/** Users who match every condition, of whom `rollout` percent get the flag. No conditions: everyone. */
|
|
45
|
+
export interface FlagConditionSet {
|
|
46
|
+
conditions: FlagCondition[];
|
|
47
|
+
/** 0 to 100. */
|
|
48
|
+
rollout: number;
|
|
49
|
+
}
|
|
50
|
+
export interface FlagConfig {
|
|
51
|
+
key: string;
|
|
52
|
+
/** Off: everyone gets the off side. */
|
|
53
|
+
on: boolean;
|
|
54
|
+
/** The first set a person matches decides; none matched: off. */
|
|
55
|
+
sets: FlagConditionSet[];
|
|
56
|
+
/** Multivariate flags: the variants and their split in percent (control included). Missing for an on/off flag. */
|
|
57
|
+
variants?: {
|
|
58
|
+
key: string;
|
|
59
|
+
split: number;
|
|
60
|
+
payload?: string;
|
|
61
|
+
}[];
|
|
62
|
+
}
|
|
63
|
+
/** What a person is for flag conditions: their properties from identify(), their id and the page they are on. */
|
|
64
|
+
export type FlagPerson = Record<string, unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* The flag for one person: `false` (off), `true` (on, an on/off flag) or the variant's key (a multivariate flag). The
|
|
67
|
+
* bucket is a stable hash of flag and person, so a person keeps their side as the rollout grows.
|
|
68
|
+
*/
|
|
69
|
+
export declare const evaluateFlag: (flag: FlagConfig, distinctId: string, person?: FlagPerson) => boolean | string;
|
|
70
|
+
/** A flag of the config for this person; an unknown flag (or no config) is off. */
|
|
71
|
+
export declare const flagFor: (key: string, config: CockpitConfig | null, distinctId: string, person?: FlagPerson) => string | boolean;
|
|
72
|
+
/** One JavaScript error as it goes over the wire to `POST /api/sdk/errors`. */
|
|
73
|
+
export interface ErrorReport {
|
|
74
|
+
/** "TypeError", "Error" … */
|
|
75
|
+
type: string;
|
|
76
|
+
message: string;
|
|
77
|
+
/** The raw stack, at most 8 KB. MRRocket parses it into frames. */
|
|
78
|
+
stack?: string;
|
|
79
|
+
/** "onerror" (uncaught), "onunhandledrejection" (a rejected promise nobody caught) or "manual" (captureError). */
|
|
80
|
+
mechanism: "onerror" | "onunhandledrejection" | "manual";
|
|
81
|
+
handled: boolean;
|
|
82
|
+
url: string;
|
|
83
|
+
ts: number;
|
|
84
|
+
/** What happened just before: pages and clicks, newest last, at most 20. */
|
|
85
|
+
crumbs?: {
|
|
86
|
+
ts: number;
|
|
87
|
+
type: "navigation" | "click" | "lifecycle";
|
|
88
|
+
message: string;
|
|
89
|
+
}[];
|
|
90
|
+
/** Your release (a version or commit), from the provider's `release` prop. */
|
|
91
|
+
release?: string;
|
|
92
|
+
distinctId?: string;
|
|
93
|
+
sessionId?: string;
|
|
32
94
|
}
|
|
33
95
|
/**
|
|
34
96
|
* Session recordings of a product, from the config. Recording also needs the visitor's own replay consent
|
|
@@ -82,11 +144,30 @@ export interface CockpitEvent {
|
|
|
82
144
|
utm_campaign?: string;
|
|
83
145
|
utm_term?: string;
|
|
84
146
|
utm_content?: string;
|
|
85
|
-
/** The screen size in CSS pixels. */
|
|
147
|
+
/** The screen size in CSS pixels (points on a phone). */
|
|
86
148
|
screen?: {
|
|
87
149
|
w: number;
|
|
88
150
|
h: number;
|
|
89
151
|
};
|
|
152
|
+
/** The app that sent it (React Native only): MRRocket stores it with the event and fills OS and device from it. */
|
|
153
|
+
app?: AppContext;
|
|
154
|
+
}
|
|
155
|
+
/** Where a React Native event comes from. Every field is optional except the platform; nothing identifies the person. */
|
|
156
|
+
export interface AppContext {
|
|
157
|
+
/** "ios" or "android" (React Native's `Platform.OS`). */
|
|
158
|
+
platform: string;
|
|
159
|
+
/** The app's version, like "1.4.0" (`expo-application`'s `nativeApplicationVersion`). */
|
|
160
|
+
appVersion?: string;
|
|
161
|
+
/** The build number, like "42". */
|
|
162
|
+
build?: string;
|
|
163
|
+
/** The OS version, like "17.4". */
|
|
164
|
+
osVersion?: string;
|
|
165
|
+
/** The device model, like "iPhone 15 Pro" (`expo-device`). */
|
|
166
|
+
deviceModel?: string;
|
|
167
|
+
/** "phone", "tablet", "desktop" or "tv" when `expo-device` knows it. */
|
|
168
|
+
deviceType?: string;
|
|
169
|
+
/** The first preferred locale, like "de-DE". */
|
|
170
|
+
locale?: string;
|
|
90
171
|
}
|
|
91
172
|
/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */
|
|
92
173
|
export declare const PREVIEW_PARAM = "ck_preview";
|
|
@@ -98,10 +179,22 @@ export declare const ID_COOKIE = "ck_id";
|
|
|
98
179
|
export declare const SESSION_COOKIE = "ck_sid";
|
|
99
180
|
/** A session ends after this long without an event. */
|
|
100
181
|
export declare const SESSION_TIMEOUT_MS: number;
|
|
182
|
+
/** The UTM parameters from a parameter lookup (`URLSearchParams.get` in a browser). Empty values are left out. */
|
|
183
|
+
export declare const utmFrom: (get: (name: string) => string | null | undefined) => Partial<Record<UtmKey, string>>;
|
|
101
184
|
/** The UTM parameters of a query string, like `"?utm_source=x"`. Empty values are left out. */
|
|
102
185
|
export declare const readUtm: (search: string) => Partial<Record<UtmKey, string>>;
|
|
186
|
+
/**
|
|
187
|
+
* The query parameters of a URL or query string, without `URLSearchParams` (React Native's is incomplete): `+` is a
|
|
188
|
+
* space, values are percent-decoded, the first of a repeated name wins.
|
|
189
|
+
*/
|
|
190
|
+
export declare const parseQuery: (url: string) => Record<string, string>;
|
|
103
191
|
/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */
|
|
104
192
|
export declare const hashUnit: (input: string) => number;
|
|
193
|
+
/**
|
|
194
|
+
* FNV-1a with murmur3's finalizer, mapped to [0, 1): ids that differ only at the end ("user-1", "user-2") still spread
|
|
195
|
+
* evenly, so a 30 % rollout reaches about 30 % of people. Flags only; experiments keep `hashUnit` so nobody switches.
|
|
196
|
+
*/
|
|
197
|
+
export declare const flagUnit: (input: string) => number;
|
|
105
198
|
/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */
|
|
106
199
|
export declare const assign: (experiment: ExperimentConfig, distinctId: string) => VariantId;
|
|
107
200
|
/** `"a:b,c:d"` → `{ a: "b", c: "d" }`. Ignores anything malformed. */
|
package/dist/core.js
CHANGED
|
@@ -1,19 +1,84 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
+
var SDK_VERSION = "0.4.0";
|
|
3
|
+
var propOf = (person, name) => {
|
|
4
|
+
const want = name.trim().toLowerCase();
|
|
5
|
+
for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
|
|
6
|
+
return void 0;
|
|
7
|
+
};
|
|
8
|
+
var same = (actual, value) => {
|
|
9
|
+
const a = actual.trim().toLowerCase();
|
|
10
|
+
const v = value.trim().toLowerCase();
|
|
11
|
+
return v.startsWith("@") ? a.endsWith(v) : a === v;
|
|
12
|
+
};
|
|
13
|
+
var matchesCondition = (c, person) => {
|
|
14
|
+
if (c.op === "in cohort") return false;
|
|
15
|
+
const raw = propOf(person, c.property);
|
|
16
|
+
const actual = raw === void 0 || raw === null ? "" : String(raw);
|
|
17
|
+
if (c.op === "\u2265") {
|
|
18
|
+
const a = Number(actual);
|
|
19
|
+
const b = Number(c.values[0]);
|
|
20
|
+
return actual !== "" && Number.isFinite(a) && Number.isFinite(b) && a >= b;
|
|
21
|
+
}
|
|
22
|
+
const hit = actual !== "" && c.values.some((v) => same(actual, v));
|
|
23
|
+
return c.op === "is" || c.op === "in" ? hit : !hit;
|
|
24
|
+
};
|
|
25
|
+
var evaluateFlag = (flag, distinctId, person = {}) => {
|
|
26
|
+
if (!flag.on) return false;
|
|
27
|
+
const subject = { id: distinctId, ...person };
|
|
28
|
+
const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
|
|
29
|
+
if (!set) return false;
|
|
30
|
+
if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;
|
|
31
|
+
if (!flag.variants?.length) return true;
|
|
32
|
+
const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;
|
|
33
|
+
let acc = 0;
|
|
34
|
+
for (const v of flag.variants) {
|
|
35
|
+
acc += v.split;
|
|
36
|
+
if (u < acc) return v.key;
|
|
37
|
+
}
|
|
38
|
+
return flag.variants[flag.variants.length - 1].key;
|
|
39
|
+
};
|
|
40
|
+
var flagFor = (key, config, distinctId, person = {}) => {
|
|
41
|
+
const flag = config?.flags?.find((f) => f.key === key);
|
|
42
|
+
return flag ? evaluateFlag(flag, distinctId, person) : false;
|
|
43
|
+
};
|
|
2
44
|
var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
|
3
45
|
var PREVIEW_PARAM = "ck_preview";
|
|
4
46
|
var EMBED_PARAM = "ck_embed";
|
|
5
47
|
var ID_COOKIE = "ck_id";
|
|
6
48
|
var SESSION_COOKIE = "ck_sid";
|
|
7
49
|
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
8
|
-
var
|
|
9
|
-
const params = new URLSearchParams(search);
|
|
50
|
+
var utmFrom = (get) => {
|
|
10
51
|
const out = {};
|
|
11
52
|
for (const key of UTM_KEYS) {
|
|
12
|
-
const value =
|
|
53
|
+
const value = get(key)?.trim();
|
|
13
54
|
if (value) out[key] = value.slice(0, 150);
|
|
14
55
|
}
|
|
15
56
|
return out;
|
|
16
57
|
};
|
|
58
|
+
var readUtm = (search) => {
|
|
59
|
+
const params = new URLSearchParams(search);
|
|
60
|
+
return utmFrom((k) => params.get(k));
|
|
61
|
+
};
|
|
62
|
+
var parseQuery = (url) => {
|
|
63
|
+
const out = {};
|
|
64
|
+
const q = url.split("#")[0];
|
|
65
|
+
const at = q.indexOf("?");
|
|
66
|
+
if (at < 0) return out;
|
|
67
|
+
const dec = (s) => {
|
|
68
|
+
try {
|
|
69
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
70
|
+
} catch {
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (const pair of q.slice(at + 1).split("&")) {
|
|
75
|
+
if (!pair) continue;
|
|
76
|
+
const eq = pair.indexOf("=");
|
|
77
|
+
const k = dec(eq < 0 ? pair : pair.slice(0, eq));
|
|
78
|
+
if (k && !(k in out)) out[k] = eq < 0 ? "" : dec(pair.slice(eq + 1));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
17
82
|
var hashUnit = (input) => {
|
|
18
83
|
let h = 2166136261;
|
|
19
84
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -22,6 +87,19 @@ var hashUnit = (input) => {
|
|
|
22
87
|
}
|
|
23
88
|
return (h >>> 0) / 4294967296;
|
|
24
89
|
};
|
|
90
|
+
var flagUnit = (input) => {
|
|
91
|
+
let h = 2166136261;
|
|
92
|
+
for (let i = 0; i < input.length; i++) {
|
|
93
|
+
h ^= input.charCodeAt(i);
|
|
94
|
+
h = Math.imul(h, 16777619);
|
|
95
|
+
}
|
|
96
|
+
h ^= h >>> 16;
|
|
97
|
+
h = Math.imul(h, 2246822507);
|
|
98
|
+
h ^= h >>> 13;
|
|
99
|
+
h = Math.imul(h, 3266489909);
|
|
100
|
+
h ^= h >>> 16;
|
|
101
|
+
return (h >>> 0) / 4294967296;
|
|
102
|
+
};
|
|
25
103
|
var assign = (experiment, distinctId) => {
|
|
26
104
|
const control = experiment.variants[0]?.id ?? "control";
|
|
27
105
|
if (experiment.winner) return experiment.winner;
|
|
@@ -86,19 +164,25 @@ export {
|
|
|
86
164
|
EMBED_PARAM,
|
|
87
165
|
ID_COOKIE,
|
|
88
166
|
PREVIEW_PARAM,
|
|
167
|
+
SDK_VERSION,
|
|
89
168
|
SESSION_COOKIE,
|
|
90
169
|
SESSION_TIMEOUT_MS,
|
|
91
170
|
UTM_KEYS,
|
|
92
171
|
assign,
|
|
93
172
|
envHost,
|
|
94
173
|
envKey,
|
|
174
|
+
evaluateFlag,
|
|
175
|
+
flagFor,
|
|
176
|
+
flagUnit,
|
|
95
177
|
hashUnit,
|
|
96
178
|
newDistinctId,
|
|
97
179
|
parsePreview,
|
|
180
|
+
parseQuery,
|
|
98
181
|
previewHref,
|
|
99
182
|
readUtm,
|
|
100
183
|
resolveHost,
|
|
101
184
|
resolveVariant,
|
|
102
|
-
textChanges
|
|
185
|
+
textChanges,
|
|
186
|
+
utmFrom
|
|
103
187
|
};
|
|
104
188
|
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\n\nexport type VariantId = string;\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n /** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */\n replay?: ReplayConfig;\n}\n\n/**\n * Session recordings of a product, from the config. Recording also needs the visitor's own replay consent\n * (`<CockpitProvider replay>` or `setConsent({ replay: true })`); without it nothing is recorded or buffered.\n */\nexport interface ReplayConfig {\n /**\n * Share of sessions uploaded from their start, 0 to 1 (default 0.1). The others keep only the last minute in\n * memory and upload it when something goes wrong: a JS error, a rage click, a `purchase…` or `checkout…` event.\n */\n sampleRate: number;\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/**\n * What `setConsent` takes: a mode as before, or the parts to change. `replay` is the separate consent for session\n * recordings (default false); it applies only while analytics is not \"off\".\n *\n * setConsent(\"cookies\");\n * setConsent({ replay: true });\n * setConsent({ analytics: \"cookieless\", replay: false });\n */\nexport type ConsentChoice = ConsentMode | { analytics?: ConsentMode; replay?: boolean };\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels. */\n screen?: { w: number; h: number };\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor, merged in config order. Control keeps the page's own words,\n * so only a non-control variant (assigned, forced in a preview, or a shipped winner) changes text.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = {};\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\n\nexport type VariantId = string;\n\n/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */\nexport const SDK_VERSION = \"0.4.0\";\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n /** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */\n replay?: ReplayConfig;\n /** Feature flags of the product in Production (MRRocket › Build › Feature flags), evaluated by `useFlag` and `getFlag`. */\n flags?: FlagConfig[];\n}\n\n/** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */\nexport interface FlagCondition {\n /** \"Email\", \"Plan\", \"Country\", \"id\", \"path\" … Matched against the person's properties without regard to case. */\n property: string;\n op: \"is\" | \"is not\" | \"in\" | \"not in\" | \"≥\" | \"in cohort\";\n values: string[];\n}\n\n/** Users who match every condition, of whom `rollout` percent get the flag. No conditions: everyone. */\nexport interface FlagConditionSet {\n conditions: FlagCondition[];\n /** 0 to 100. */\n rollout: number;\n}\n\nexport interface FlagConfig {\n key: string;\n /** Off: everyone gets the off side. */\n on: boolean;\n /** The first set a person matches decides; none matched: off. */\n sets: FlagConditionSet[];\n /** Multivariate flags: the variants and their split in percent (control included). Missing for an on/off flag. */\n variants?: { key: string; split: number; payload?: string }[];\n}\n\n/** What a person is for flag conditions: their properties from identify(), their id and the page they are on. */\nexport type FlagPerson = Record<string, unknown>;\n\nconst propOf = (person: FlagPerson, name: string) => {\n const want = name.trim().toLowerCase();\n for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;\n return undefined;\n};\n\n/** `\"@acme.com\"` matches every address at that domain; anything else matches the whole value, without regard to case. */\nconst same = (actual: string, value: string) => {\n const a = actual.trim().toLowerCase();\n const v = value.trim().toLowerCase();\n return v.startsWith(\"@\") ? a.endsWith(v) : a === v;\n};\n\nconst matchesCondition = (c: FlagCondition, person: FlagPerson) => {\n // Cohorts live in MRRocket; a browser cannot know them, so a cohort condition never matches here.\n if (c.op === \"in cohort\") return false;\n const raw = propOf(person, c.property);\n const actual = raw === undefined || raw === null ? \"\" : String(raw);\n if (c.op === \"≥\") {\n const a = Number(actual);\n const b = Number(c.values[0]);\n return actual !== \"\" && Number.isFinite(a) && Number.isFinite(b) && a >= b;\n }\n const hit = actual !== \"\" && c.values.some((v) => same(actual, v));\n return c.op === \"is\" || c.op === \"in\" ? hit : !hit;\n};\n\n/**\n * The flag for one person: `false` (off), `true` (on, an on/off flag) or the variant's key (a multivariate flag). The\n * bucket is a stable hash of flag and person, so a person keeps their side as the rollout grows.\n */\nexport const evaluateFlag = (flag: FlagConfig, distinctId: string, person: FlagPerson = {}): boolean | string => {\n if (!flag.on) return false;\n const subject = { id: distinctId, ...person };\n const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };\n if (!set) return false;\n if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;\n if (!flag.variants?.length) return true;\n const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;\n let acc = 0;\n for (const v of flag.variants) {\n acc += v.split;\n if (u < acc) return v.key;\n }\n return flag.variants[flag.variants.length - 1].key;\n};\n\n/** A flag of the config for this person; an unknown flag (or no config) is off. */\nexport const flagFor = (key: string, config: CockpitConfig | null, distinctId: string, person: FlagPerson = {}) => {\n const flag = config?.flags?.find((f) => f.key === key);\n return flag ? evaluateFlag(flag, distinctId, person) : false;\n};\n\n/** One JavaScript error as it goes over the wire to `POST /api/sdk/errors`. */\nexport interface ErrorReport {\n /** \"TypeError\", \"Error\" … */\n type: string;\n message: string;\n /** The raw stack, at most 8 KB. MRRocket parses it into frames. */\n stack?: string;\n /** \"onerror\" (uncaught), \"onunhandledrejection\" (a rejected promise nobody caught) or \"manual\" (captureError). */\n mechanism: \"onerror\" | \"onunhandledrejection\" | \"manual\";\n handled: boolean;\n url: string;\n ts: number;\n /** What happened just before: pages and clicks, newest last, at most 20. */\n crumbs?: { ts: number; type: \"navigation\" | \"click\" | \"lifecycle\"; message: string }[];\n /** Your release (a version or commit), from the provider's `release` prop. */\n release?: string;\n distinctId?: string;\n sessionId?: string;\n}\n\n/**\n * Session recordings of a product, from the config. Recording also needs the visitor's own replay consent\n * (`<CockpitProvider replay>` or `setConsent({ replay: true })`); without it nothing is recorded or buffered.\n */\nexport interface ReplayConfig {\n /**\n * Share of sessions uploaded from their start, 0 to 1 (default 0.1). The others keep only the last minute in\n * memory and upload it when something goes wrong: a JS error, a rage click, a `purchase…` or `checkout…` event.\n */\n sampleRate: number;\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/**\n * What `setConsent` takes: a mode as before, or the parts to change. `replay` is the separate consent for session\n * recordings (default false); it applies only while analytics is not \"off\".\n *\n * setConsent(\"cookies\");\n * setConsent({ replay: true });\n * setConsent({ analytics: \"cookieless\", replay: false });\n */\nexport type ConsentChoice = ConsentMode | { analytics?: ConsentMode; replay?: boolean };\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels (points on a phone). */\n screen?: { w: number; h: number };\n /** The app that sent it (React Native only): MRRocket stores it with the event and fills OS and device from it. */\n app?: AppContext;\n}\n\n/** Where a React Native event comes from. Every field is optional except the platform; nothing identifies the person. */\nexport interface AppContext {\n /** \"ios\" or \"android\" (React Native's `Platform.OS`). */\n platform: string;\n /** The app's version, like \"1.4.0\" (`expo-application`'s `nativeApplicationVersion`). */\n appVersion?: string;\n /** The build number, like \"42\". */\n build?: string;\n /** The OS version, like \"17.4\". */\n osVersion?: string;\n /** The device model, like \"iPhone 15 Pro\" (`expo-device`). */\n deviceModel?: string;\n /** \"phone\", \"tablet\", \"desktop\" or \"tv\" when `expo-device` knows it. */\n deviceType?: string;\n /** The first preferred locale, like \"de-DE\". */\n locale?: string;\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters from a parameter lookup (`URLSearchParams.get` in a browser). Empty values are left out. */\nexport const utmFrom = (get: (name: string) => string | null | undefined): Partial<Record<UtmKey, string>> => {\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n return utmFrom((k) => params.get(k));\n};\n\n/**\n * The query parameters of a URL or query string, without `URLSearchParams` (React Native's is incomplete): `+` is a\n * space, values are percent-decoded, the first of a repeated name wins.\n */\nexport const parseQuery = (url: string): Record<string, string> => {\n const out: Record<string, string> = {};\n const q = url.split(\"#\")[0];\n const at = q.indexOf(\"?\");\n if (at < 0) return out;\n const dec = (s: string) => {\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return s;\n }\n };\n for (const pair of q.slice(at + 1).split(\"&\")) {\n if (!pair) continue;\n const eq = pair.indexOf(\"=\");\n const k = dec(eq < 0 ? pair : pair.slice(0, eq));\n if (k && !(k in out)) out[k] = eq < 0 ? \"\" : dec(pair.slice(eq + 1));\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/**\n * FNV-1a with murmur3's finalizer, mapped to [0, 1): ids that differ only at the end (\"user-1\", \"user-2\") still spread\n * evenly, so a 30 % rollout reaches about 30 % of people. Flags only; experiments keep `hashUnit` so nobody switches.\n */\nexport const flagUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor, merged in config order. Control keeps the page's own words,\n * so only a non-control variant (assigned, forced in a preview, or a shipped winner) changes text.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = {};\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n"],"mappings":"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","names":[]}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error tracking: uncaught errors and unhandled promise rejections of the page (and `captureError` for ones you
|
|
3
|
+
* catch), sent to `POST /api/sdk/errors`, where MRRocket groups them into issues for Build › Errors. Independent of
|
|
4
|
+
* session recordings. Only the error itself, the page URL and the last pages and clicks go along (no typed text, no
|
|
5
|
+
* network bodies), under the same consent as analytics.
|
|
6
|
+
*
|
|
7
|
+
* Limits on the page: the same error is sent once per page load, at most 20 errors per page load, in batches every
|
|
8
|
+
* 2 seconds and with `sendBeacon` when the page hides.
|
|
9
|
+
*/
|
|
10
|
+
import type { ErrorReport } from "./core.js";
|
|
11
|
+
export interface ErrorCapture {
|
|
12
|
+
/** An error you caught yourself; `handled` is true. */
|
|
13
|
+
capture: (error: unknown, mechanism?: ErrorReport["mechanism"]) => void;
|
|
14
|
+
/** A page view or a click for the breadcrumbs. */
|
|
15
|
+
crumb: (type: "navigation" | "click" | "lifecycle", message: string) => void;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Type, message and stack of anything thrown. */
|
|
19
|
+
export declare const describeError: (error: unknown) => {
|
|
20
|
+
type: string;
|
|
21
|
+
message: string;
|
|
22
|
+
stack?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const startErrorCapture: (opts: {
|
|
25
|
+
host: string;
|
|
26
|
+
apiKey: string;
|
|
27
|
+
release?: string;
|
|
28
|
+
/** Read when an error happens: whether to send at all (consent), and who and which session. */
|
|
29
|
+
context: () => {
|
|
30
|
+
send: boolean;
|
|
31
|
+
distinctId?: string;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
};
|
|
34
|
+
warn: (message: string) => void;
|
|
35
|
+
}) => ErrorCapture;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var MAX_STACK = 8 * 1024;
|
|
3
|
+
var MAX_PER_PAGE = 20;
|
|
4
|
+
var MAX_CRUMBS = 20;
|
|
5
|
+
var FLUSH_MS = 2e3;
|
|
6
|
+
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
7
|
+
var describeError = (error) => {
|
|
8
|
+
if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
|
|
9
|
+
if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
|
|
10
|
+
try {
|
|
11
|
+
return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
|
|
12
|
+
} catch {
|
|
13
|
+
return { type: "Error", message: String(error) };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var startErrorCapture = (opts) => {
|
|
17
|
+
const queue = [];
|
|
18
|
+
const crumbs = [];
|
|
19
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20
|
+
let sent = 0;
|
|
21
|
+
const url = `${opts.host}/api/sdk/errors`;
|
|
22
|
+
const flush = (beacon = false) => {
|
|
23
|
+
while (queue.length) {
|
|
24
|
+
const body = JSON.stringify({ key: opts.apiKey, errors: queue.splice(0, 4) });
|
|
25
|
+
const ok = beacon && typeof navigator !== "undefined" && "sendBeacon" in navigator && navigator.sendBeacon(url, body);
|
|
26
|
+
if (!ok)
|
|
27
|
+
fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).then((r) => {
|
|
28
|
+
if (!r.ok && r.status !== 429) opts.warn(`Error reports were rejected with ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);
|
|
29
|
+
}).catch(() => {
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const capture = (error, mechanism = "manual") => {
|
|
34
|
+
const ctx = opts.context();
|
|
35
|
+
if (!ctx.send || sent >= MAX_PER_PAGE) return;
|
|
36
|
+
const d = describeError(error);
|
|
37
|
+
if (/\/api\/sdk\/errors/.test(d.message)) return;
|
|
38
|
+
const mark = `${d.type}:${d.message}`;
|
|
39
|
+
if (seen.has(mark)) return;
|
|
40
|
+
seen.add(mark);
|
|
41
|
+
sent++;
|
|
42
|
+
queue.push({
|
|
43
|
+
...d,
|
|
44
|
+
mechanism,
|
|
45
|
+
handled: mechanism === "manual",
|
|
46
|
+
url: window.location.href,
|
|
47
|
+
ts: Date.now(),
|
|
48
|
+
crumbs: crumbs.slice(),
|
|
49
|
+
...opts.release ? { release: opts.release } : {},
|
|
50
|
+
...ctx.distinctId ? { distinctId: ctx.distinctId } : {},
|
|
51
|
+
...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const onError = (e) => {
|
|
55
|
+
if (!e.error && (!e.message || e.message === "Script error.")) return;
|
|
56
|
+
capture(e.error ?? new Error(e.message), "onerror");
|
|
57
|
+
};
|
|
58
|
+
const onRejection = (e) => capture(e.reason ?? new Error("Unhandled promise rejection"), "onunhandledrejection");
|
|
59
|
+
const onHide = () => flush(true);
|
|
60
|
+
window.addEventListener("error", onError);
|
|
61
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
62
|
+
window.addEventListener("pagehide", onHide);
|
|
63
|
+
const timer = window.setInterval(() => flush(), FLUSH_MS);
|
|
64
|
+
return {
|
|
65
|
+
capture,
|
|
66
|
+
crumb: (type, message) => {
|
|
67
|
+
crumbs.push({ ts: Date.now(), type, message: clip(message, 120) });
|
|
68
|
+
if (crumbs.length > MAX_CRUMBS) crumbs.shift();
|
|
69
|
+
},
|
|
70
|
+
stop: () => {
|
|
71
|
+
window.removeEventListener("error", onError);
|
|
72
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
73
|
+
window.removeEventListener("pagehide", onHide);
|
|
74
|
+
window.clearInterval(timer);
|
|
75
|
+
flush(true);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export {
|
|
80
|
+
describeError,
|
|
81
|
+
startErrorCapture
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error tracking: uncaught errors and unhandled promise rejections of the page (and `captureError` for ones you\n * catch), sent to `POST /api/sdk/errors`, where MRRocket groups them into issues for Build › Errors. Independent of\n * session recordings. Only the error itself, the page URL and the last pages and clicks go along (no typed text, no\n * network bodies), under the same consent as analytics.\n *\n * Limits on the page: the same error is sent once per page load, at most 20 errors per page load, in batches every\n * 2 seconds and with `sendBeacon` when the page hides.\n */\nimport type { ErrorReport } from \"./core\";\n\nconst MAX_STACK = 8 * 1024;\nconst MAX_PER_PAGE = 20;\nconst MAX_CRUMBS = 20;\nconst FLUSH_MS = 2000;\n\nexport interface ErrorCapture {\n /** An error you caught yourself; `handled` is true. */\n capture: (error: unknown, mechanism?: ErrorReport[\"mechanism\"]) => void;\n /** A page view or a click for the breadcrumbs. */\n crumb: (type: \"navigation\" | \"click\" | \"lifecycle\", message: string) => void;\n stop: () => void;\n}\n\nconst clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);\n\n/** Type, message and stack of anything thrown. */\nexport const describeError = (error: unknown): { type: string; message: string; stack?: string } => {\n if (error instanceof Error) return { type: error.name || \"Error\", message: clip(error.message || String(error), 1000), stack: error.stack ? clip(error.stack, MAX_STACK) : undefined };\n if (typeof error === \"string\") return { type: \"Error\", message: clip(error, 1000) };\n try {\n return { type: \"Error\", message: clip(JSON.stringify(error) ?? String(error), 1000) };\n } catch {\n return { type: \"Error\", message: String(error) };\n }\n};\n\nexport const startErrorCapture = (opts: {\n host: string;\n apiKey: string;\n release?: string;\n /** Read when an error happens: whether to send at all (consent), and who and which session. */\n context: () => { send: boolean; distinctId?: string; sessionId?: string };\n warn: (message: string) => void;\n}): ErrorCapture => {\n const queue: ErrorReport[] = [];\n const crumbs: NonNullable<ErrorReport[\"crumbs\"]> = [];\n const seen = new Set<string>();\n let sent = 0;\n const url = `${opts.host}/api/sdk/errors`;\n\n const flush = (beacon = false) => {\n // At most four reports per request: four full stacks stay under the server's 64 KB.\n while (queue.length) {\n const body = JSON.stringify({ key: opts.apiKey, errors: queue.splice(0, 4) });\n const ok = beacon && typeof navigator !== \"undefined\" && \"sendBeacon\" in navigator && navigator.sendBeacon(url, body);\n if (!ok)\n fetch(url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, keepalive: true })\n .then((r) => {\n if (!r.ok && r.status !== 429) opts.warn(`Error reports were rejected with ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);\n })\n .catch(() => {});\n }\n };\n\n const capture = (error: unknown, mechanism: ErrorReport[\"mechanism\"] = \"manual\") => {\n const ctx = opts.context();\n if (!ctx.send || sent >= MAX_PER_PAGE) return;\n const d = describeError(error);\n // Our own reports failing must never report themselves.\n if (/\\/api\\/sdk\\/errors/.test(d.message)) return;\n const mark = `${d.type}:${d.message}`;\n if (seen.has(mark)) return;\n seen.add(mark);\n sent++;\n queue.push({\n ...d,\n mechanism,\n handled: mechanism === \"manual\",\n url: window.location.href,\n ts: Date.now(),\n crumbs: crumbs.slice(),\n ...(opts.release ? { release: opts.release } : {}),\n ...(ctx.distinctId ? { distinctId: ctx.distinctId } : {}),\n ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),\n });\n };\n\n const onError = (e: ErrorEvent) => {\n // \"Script error.\" from a cross-origin script carries nothing useful.\n if (!e.error && (!e.message || e.message === \"Script error.\")) return;\n capture(e.error ?? new Error(e.message), \"onerror\");\n };\n const onRejection = (e: PromiseRejectionEvent) => capture(e.reason ?? new Error(\"Unhandled promise rejection\"), \"onunhandledrejection\");\n const onHide = () => flush(true);\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n window.addEventListener(\"pagehide\", onHide);\n const timer = window.setInterval(() => flush(), FLUSH_MS);\n\n return {\n capture,\n crumb: (type, message) => {\n crumbs.push({ ts: Date.now(), type, message: clip(message, 120) });\n if (crumbs.length > MAX_CRUMBS) crumbs.shift();\n },\n stop: () => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n window.removeEventListener(\"pagehide\", onHide);\n window.clearInterval(timer);\n flush(true);\n },\n };\n};\n"],"mappings":";AAWA,IAAM,YAAY,IAAI;AACtB,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,WAAW;AAUjB,IAAM,OAAO,CAAC,GAAW,MAAe,EAAE,SAAS,IAAI,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,WAAM;AAG1E,IAAM,gBAAgB,CAAC,UAAsE;AAChG,MAAI,iBAAiB,MAAO,QAAO,EAAE,MAAM,MAAM,QAAQ,SAAS,SAAS,KAAK,MAAM,WAAW,OAAO,KAAK,GAAG,GAAI,GAAG,OAAO,MAAM,QAAQ,KAAK,MAAM,OAAO,SAAS,IAAI,OAAU;AACrL,MAAI,OAAO,UAAU,SAAU,QAAO,EAAE,MAAM,SAAS,SAAS,KAAK,OAAO,GAAI,EAAE;AAClF,MAAI;AACA,WAAO,EAAE,MAAM,SAAS,SAAS,KAAK,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK,GAAG,GAAI,EAAE;AAAA,EACxF,QAAQ;AACJ,WAAO,EAAE,MAAM,SAAS,SAAS,OAAO,KAAK,EAAE;AAAA,EACnD;AACJ;AAEO,IAAM,oBAAoB,CAAC,SAOd;AAChB,QAAM,QAAuB,CAAC;AAC9B,QAAM,SAA6C,CAAC;AACpD,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,OAAO;AACX,QAAM,MAAM,GAAG,KAAK,IAAI;AAExB,QAAM,QAAQ,CAAC,SAAS,UAAU;AAE9B,WAAO,MAAM,QAAQ;AACjB,YAAM,OAAO,KAAK,UAAU,EAAE,KAAK,KAAK,QAAQ,QAAQ,MAAM,OAAO,GAAG,CAAC,EAAE,CAAC;AAC5E,YAAM,KAAK,UAAU,OAAO,cAAc,eAAe,gBAAgB,aAAa,UAAU,WAAW,KAAK,IAAI;AACpH,UAAI,CAAC;AACD,cAAM,KAAK,EAAE,QAAQ,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,GAAG,MAAM,WAAW,KAAK,CAAC,EAChG,KAAK,CAAC,MAAM;AACT,cAAI,CAAC,EAAE,MAAM,EAAE,WAAW,IAAK,MAAK,KAAK,oCAAoC,EAAE,MAAM,uDAAuD;AAAA,QAChJ,CAAC,EACA,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,IAC3B;AAAA,EACJ;AAEA,QAAM,UAAU,CAAC,OAAgB,YAAsC,aAAa;AAChF,UAAM,MAAM,KAAK,QAAQ;AACzB,QAAI,CAAC,IAAI,QAAQ,QAAQ,aAAc;AACvC,UAAM,IAAI,cAAc,KAAK;AAE7B,QAAI,qBAAqB,KAAK,EAAE,OAAO,EAAG;AAC1C,UAAM,OAAO,GAAG,EAAE,IAAI,IAAI,EAAE,OAAO;AACnC,QAAI,KAAK,IAAI,IAAI,EAAG;AACpB,SAAK,IAAI,IAAI;AACb;AACA,UAAM,KAAK;AAAA,MACP,GAAG;AAAA,MACH;AAAA,MACA,SAAS,cAAc;AAAA,MACvB,KAAK,OAAO,SAAS;AAAA,MACrB,IAAI,KAAK,IAAI;AAAA,MACb,QAAQ,OAAO,MAAM;AAAA,MACrB,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,MAChD,GAAI,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,IAAI,CAAC;AAAA,MACvD,GAAI,IAAI,YAAY,EAAE,WAAW,IAAI,UAAU,IAAI,CAAC;AAAA,IACxD,CAAC;AAAA,EACL;AAEA,QAAM,UAAU,CAAC,MAAkB;AAE/B,QAAI,CAAC,EAAE,UAAU,CAAC,EAAE,WAAW,EAAE,YAAY,iBAAkB;AAC/D,YAAQ,EAAE,SAAS,IAAI,MAAM,EAAE,OAAO,GAAG,SAAS;AAAA,EACtD;AACA,QAAM,cAAc,CAAC,MAA6B,QAAQ,EAAE,UAAU,IAAI,MAAM,6BAA6B,GAAG,sBAAsB;AACtI,QAAM,SAAS,MAAM,MAAM,IAAI;AAC/B,SAAO,iBAAiB,SAAS,OAAO;AACxC,SAAO,iBAAiB,sBAAsB,WAAW;AACzD,SAAO,iBAAiB,YAAY,MAAM;AAC1C,QAAM,QAAQ,OAAO,YAAY,MAAM,MAAM,GAAG,QAAQ;AAExD,SAAO;AAAA,IACH;AAAA,IACA,OAAO,CAAC,MAAM,YAAY;AACtB,aAAO,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG,MAAM,SAAS,KAAK,SAAS,GAAG,EAAE,CAAC;AACjE,UAAI,OAAO,SAAS,WAAY,QAAO,MAAM;AAAA,IACjD;AAAA,IACA,MAAM,MAAM;AACR,aAAO,oBAAoB,SAAS,OAAO;AAC3C,aAAO,oBAAoB,sBAAsB,WAAW;AAC5D,aAAO,oBAAoB,YAAY,MAAM;AAC7C,aAAO,cAAc,KAAK;AAC1B,YAAM,IAAI;AAAA,IACd;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { type ChangelogEntry, type CockpitProviderProps, type CockpitUser, type FeedbackBoardInfo, type FeedbackBoardProps, type FeedbackComment, type FeedbackList, type FeedbackPost, type FeedbackRoadmap, type FeedbackWidgetProps, type PostStatus, type SurveyAnswer, type SurveyDefinition, type SurveyQuestion, type SurveyQuestionKind, CockpitProvider, Experiment, FeedbackBoard, FeedbackWidget, Survey, Text, WIDGET_CSS, createApi, useCockpit, useFeedback, useStyles, useVariant, } from "./browser.js";
|
|
2
|
-
export { type CockpitConfig, type CockpitEvent, type ConsentChoice, type ConsentMode, type ReplayConfig, type ExperimentConfig, type VariantConfig, type VariantId, EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, SESSION_COOKIE, assign, parsePreview, previewHref, } from "./core.js";
|
|
1
|
+
export { type ChangelogEntry, type CockpitProviderProps, type CockpitUser, type FeedbackBoardInfo, type FeedbackBoardProps, type FeedbackComment, type FeedbackList, type FeedbackPost, type FeedbackRoadmap, type FeedbackWidgetProps, type PostStatus, type SurveyAnswer, type SurveyDefinition, type SurveyQuestion, type SurveyQuestionKind, CockpitProvider, Experiment, FeedbackBoard, FeedbackWidget, Survey, Text, WIDGET_CSS, createApi, useCockpit, useFeedback, useFlag, useFlagVariant, useStyles, useVariant, } from "./browser.js";
|
|
2
|
+
export { type CockpitConfig, type CockpitEvent, type ConsentChoice, type ConsentMode, type ReplayConfig, type ErrorReport, type ExperimentConfig, type FlagCondition, type FlagConditionSet, type FlagConfig, type FlagPerson, type VariantConfig, type VariantId, EMBED_PARAM, SDK_VERSION, ID_COOKIE, PREVIEW_PARAM, SESSION_COOKIE, assign, evaluateFlag, parsePreview, previewHref, } from "./core.js";
|
|
3
3
|
export { type AttributionMode, type CheckoutReferral, type Referral, REFERRAL_COOKIE, getReferral, readReferralCookie, referralForCheckout, referralLink, } from "./referral.js";
|
package/dist/index.js
CHANGED
|
@@ -10,15 +10,19 @@ import {
|
|
|
10
10
|
createApi,
|
|
11
11
|
useCockpit,
|
|
12
12
|
useFeedback,
|
|
13
|
+
useFlag,
|
|
14
|
+
useFlagVariant,
|
|
13
15
|
useStyles,
|
|
14
16
|
useVariant
|
|
15
17
|
} from "./browser.js";
|
|
16
18
|
import {
|
|
17
19
|
EMBED_PARAM,
|
|
20
|
+
SDK_VERSION,
|
|
18
21
|
ID_COOKIE,
|
|
19
22
|
PREVIEW_PARAM,
|
|
20
23
|
SESSION_COOKIE,
|
|
21
24
|
assign,
|
|
25
|
+
evaluateFlag,
|
|
22
26
|
parsePreview,
|
|
23
27
|
previewHref
|
|
24
28
|
} from "./core.js";
|
|
@@ -38,12 +42,14 @@ export {
|
|
|
38
42
|
ID_COOKIE,
|
|
39
43
|
PREVIEW_PARAM,
|
|
40
44
|
REFERRAL_COOKIE,
|
|
45
|
+
SDK_VERSION,
|
|
41
46
|
SESSION_COOKIE,
|
|
42
47
|
Survey,
|
|
43
48
|
Text,
|
|
44
49
|
WIDGET_CSS,
|
|
45
50
|
assign,
|
|
46
51
|
createApi,
|
|
52
|
+
evaluateFlag,
|
|
47
53
|
getReferral,
|
|
48
54
|
parsePreview,
|
|
49
55
|
previewHref,
|
|
@@ -52,6 +58,8 @@ export {
|
|
|
52
58
|
referralLink,
|
|
53
59
|
useCockpit,
|
|
54
60
|
useFeedback,
|
|
61
|
+
useFlag,
|
|
62
|
+
useFlagVariant,
|
|
55
63
|
useStyles,
|
|
56
64
|
useVariant
|
|
57
65
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n type ChangelogEntry,\n type CockpitProviderProps,\n type CockpitUser,\n type FeedbackBoardInfo,\n type FeedbackBoardProps,\n type FeedbackComment,\n type FeedbackList,\n type FeedbackPost,\n type FeedbackRoadmap,\n type FeedbackWidgetProps,\n type PostStatus,\n type SurveyAnswer,\n type SurveyDefinition,\n type SurveyQuestion,\n type SurveyQuestionKind,\n CockpitProvider,\n Experiment,\n FeedbackBoard,\n FeedbackWidget,\n Survey,\n Text,\n WIDGET_CSS,\n createApi,\n useCockpit,\n useFeedback,\n useStyles,\n useVariant,\n} from \"./browser\";\nexport {\n type CockpitConfig,\n type CockpitEvent,\n type ConsentChoice,\n type ConsentMode,\n type ReplayConfig,\n type ExperimentConfig,\n type VariantConfig,\n type VariantId,\n EMBED_PARAM,\n ID_COOKIE,\n PREVIEW_PARAM,\n SESSION_COOKIE,\n assign,\n parsePreview,\n previewHref,\n} from \"./core\";\nexport {\n type AttributionMode,\n type CheckoutReferral,\n type Referral,\n REFERRAL_COOKIE,\n getReferral,\n readReferralCookie,\n referralForCheckout,\n referralLink,\n} from \"./referral\";\n"],"mappings":";AAAA;AAAA,EAgBI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n type ChangelogEntry,\n type CockpitProviderProps,\n type CockpitUser,\n type FeedbackBoardInfo,\n type FeedbackBoardProps,\n type FeedbackComment,\n type FeedbackList,\n type FeedbackPost,\n type FeedbackRoadmap,\n type FeedbackWidgetProps,\n type PostStatus,\n type SurveyAnswer,\n type SurveyDefinition,\n type SurveyQuestion,\n type SurveyQuestionKind,\n CockpitProvider,\n Experiment,\n FeedbackBoard,\n FeedbackWidget,\n Survey,\n Text,\n WIDGET_CSS,\n createApi,\n useCockpit,\n useFeedback,\n useFlag,\n useFlagVariant,\n useStyles,\n useVariant,\n} from \"./browser\";\nexport {\n type CockpitConfig,\n type CockpitEvent,\n type ConsentChoice,\n type ConsentMode,\n type ReplayConfig,\n type ErrorReport,\n type ExperimentConfig,\n type FlagCondition,\n type FlagConditionSet,\n type FlagConfig,\n type FlagPerson,\n type VariantConfig,\n type VariantId,\n EMBED_PARAM,\n SDK_VERSION,\n ID_COOKIE,\n PREVIEW_PARAM,\n SESSION_COOKIE,\n assign,\n evaluateFlag,\n parsePreview,\n previewHref,\n} from \"./core\";\nexport {\n type AttributionMode,\n type CheckoutReferral,\n type Referral,\n REFERRAL_COOKIE,\n getReferral,\n readReferralCookie,\n referralForCheckout,\n referralLink,\n} from \"./referral\";\n"],"mappings":";AAAA;AAAA,EAgBI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EAcI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EAII;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;","names":[]}
|