@mrrocket/sdk 0.0.0-stage → 0.1.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/dist/client.js ADDED
@@ -0,0 +1,373 @@
1
+ "use client";
2
+
3
+ // src/client.tsx
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { usePathname } from "next/navigation";
6
+ import { Ctx, useCtx } from "./context.js";
7
+ import { Survey, forgetStoredDismissals } from "./survey.js";
8
+ import {
9
+ DEFAULT_REFERRAL_DAYS,
10
+ captureReferral,
11
+ carriesReferral,
12
+ forgetStoredReferral,
13
+ getReferral,
14
+ resetReferral,
15
+ storeReferral
16
+ } from "./referral.js";
17
+ import {
18
+ EMBED_PARAM,
19
+ ID_COOKIE,
20
+ PREVIEW_PARAM,
21
+ SESSION_COOKIE,
22
+ SESSION_TIMEOUT_MS,
23
+ envKey,
24
+ newDistinctId,
25
+ parsePreview,
26
+ readUtm,
27
+ resolveHost,
28
+ resolveVariant,
29
+ textChanges
30
+ } from "./core.js";
31
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
32
+ var YEAR_S = 60 * 60 * 24 * 365;
33
+ var MAX_BATCH = 100;
34
+ var MAX_BYTES = 6e4;
35
+ var readCookie = (name) => {
36
+ if (typeof document === "undefined") return void 0;
37
+ const raw = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1);
38
+ if (!raw) return void 0;
39
+ try {
40
+ return decodeURIComponent(raw);
41
+ } catch {
42
+ return raw;
43
+ }
44
+ };
45
+ var writeCookie = (name, value, maxAge) => {
46
+ const secure = window.location.protocol === "https:" ? "; secure" : "";
47
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
48
+ };
49
+ var deleteCookie = (name) => {
50
+ document.cookie = `${name}=; path=/; max-age=0; samesite=lax`;
51
+ };
52
+ var page = { utm: {} };
53
+ var memoryId = () => page.id ??= newDistinctId();
54
+ var visitorId = (mode, write) => {
55
+ if (mode !== "cookies") return memoryId();
56
+ const stored = readCookie(ID_COOKIE);
57
+ if (stored) return page.id = stored;
58
+ const id = memoryId();
59
+ if (write) writeCookie(ID_COOKIE, id, YEAR_S);
60
+ return id;
61
+ };
62
+ var touchSession = (persist) => {
63
+ const now = Date.now();
64
+ const s = page.session;
65
+ const id = (persist ? readCookie(SESSION_COOKIE) : void 0) ?? (s && now - s.last < SESSION_TIMEOUT_MS ? s.id : newDistinctId());
66
+ page.session = { id, last: now };
67
+ if (persist) writeCookie(SESSION_COOKIE, id, SESSION_TIMEOUT_MS / 1e3);
68
+ return id;
69
+ };
70
+ var enterPage = () => {
71
+ const href = window.location.href;
72
+ if (page.url === href) return;
73
+ page.referrer = page.url ?? (document.referrer || void 0);
74
+ page.url = href;
75
+ const utm = readUtm(window.location.search);
76
+ if (Object.keys(utm).length) page.utm = utm;
77
+ };
78
+ var privacySignal = () => {
79
+ const nav = navigator;
80
+ const win = window;
81
+ return nav.doNotTrack === "1" || win.doNotTrack === "1" || nav.globalPrivacyControl === true;
82
+ };
83
+ var warned = /* @__PURE__ */ new Set();
84
+ var warnOnce = (message) => {
85
+ if (warned.has(message)) return;
86
+ warned.add(message);
87
+ console.warn(`[MRRocket] ${message}`);
88
+ };
89
+ var byteLength = (s) => new TextEncoder().encode(s).length;
90
+ var bodies = (key, events) => {
91
+ const head = `{"key":${JSON.stringify(key)},"events":[`;
92
+ const empty = byteLength(head) + 2;
93
+ const out = [];
94
+ let chunk = [];
95
+ let size = empty;
96
+ const close = () => {
97
+ if (chunk.length) out.push(`${head}${chunk.join(",")}]}`);
98
+ chunk = [];
99
+ size = empty;
100
+ };
101
+ for (const event of events) {
102
+ const json = JSON.stringify(event);
103
+ const n = byteLength(json) + 1;
104
+ if (empty + n > MAX_BYTES) continue;
105
+ if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
106
+ chunk.push(json);
107
+ size += n;
108
+ }
109
+ close();
110
+ return out;
111
+ };
112
+ var CockpitProvider = ({
113
+ apiKey: apiKeyProp,
114
+ host: hostProp,
115
+ preview: forced,
116
+ embedded: embeddedProp,
117
+ config: bootstrapConfig,
118
+ distinctId: bootstrapId,
119
+ consent: consentProp = "cookieless",
120
+ respectDnt = false,
121
+ surveys = true,
122
+ attribution = "consent",
123
+ referralParam = "ref",
124
+ children
125
+ }) => {
126
+ const pathname = usePathname();
127
+ const apiKey = apiKeyProp ?? envKey() ?? "";
128
+ const host = resolveHost(hostProp);
129
+ const [config, setConfig] = useState(bootstrapConfig ?? null);
130
+ const [consent, setConsentState] = useState(consentProp);
131
+ const [mounted, setMounted] = useState(false);
132
+ const [dnt, setDnt] = useState(false);
133
+ const [visitor, setVisitor] = useState(null);
134
+ const [identified, setIdentified] = useState();
135
+ const [urlPreview, setUrlPreview] = useState({});
136
+ const [urlEmbedded, setUrlEmbedded] = useState(false);
137
+ const [user, setUser] = useState();
138
+ const [ready, setReady] = useState(!!bootstrapConfig);
139
+ const queue = useRef([]);
140
+ const early = useRef([]);
141
+ const exposed = useRef(/* @__PURE__ */ new Set());
142
+ const lastPage = useRef(null);
143
+ const bootstrap = bootstrapId && bootstrapId !== "anonymous" ? bootstrapId : void 0;
144
+ const preview = useMemo(() => ({ ...urlPreview, ...forced }), [urlPreview, forced]);
145
+ const embedded = embeddedProp ?? urlEmbedded;
146
+ const effectiveConsent = respectDnt && dnt ? "off" : consent;
147
+ const distinctId = identified ?? bootstrap ?? visitor?.id ?? "anonymous";
148
+ const tracking = !embedded && effectiveConsent !== "off" && visitor?.mode === effectiveConsent;
149
+ const sendId = effectiveConsent === "cookies" ? distinctId : identified;
150
+ useEffect(() => {
151
+ const params = new URLSearchParams(window.location.search);
152
+ setUrlPreview(parsePreview(params.get(PREVIEW_PARAM)));
153
+ setUrlEmbedded(params.get(EMBED_PARAM) === "1");
154
+ setDnt(privacySignal());
155
+ setMounted(true);
156
+ }, []);
157
+ const setConsent = useCallback((mode) => {
158
+ if (mode === "off") queue.current.length = 0;
159
+ setConsentState(mode);
160
+ }, []);
161
+ const lastProp = useRef(consentProp);
162
+ useEffect(() => {
163
+ if (lastProp.current === consentProp) return;
164
+ lastProp.current = consentProp;
165
+ setConsent(consentProp);
166
+ }, [consentProp, setConsent]);
167
+ const storedFor = useRef(null);
168
+ useEffect(() => {
169
+ if (!mounted) return;
170
+ const before = storedFor.current;
171
+ storedFor.current = effectiveConsent;
172
+ if (before === "cookies" && effectiveConsent !== "cookies") {
173
+ deleteCookie(ID_COOKIE);
174
+ deleteCookie(SESSION_COOKIE);
175
+ forgetStoredDismissals();
176
+ }
177
+ if (effectiveConsent === "off") {
178
+ queue.current.length = 0;
179
+ page.session = void 0;
180
+ }
181
+ setVisitor({ mode: effectiveConsent, id: visitorId(effectiveConsent, !bootstrap) });
182
+ }, [mounted, effectiveConsent, bootstrap]);
183
+ useEffect(() => {
184
+ if (!apiKey) warnOnce("No public key. Pass apiKey or set NEXT_PUBLIC_MRROCKET_KEY; nothing is tracked until then.");
185
+ if (bootstrapConfig || !apiKey) {
186
+ setReady(true);
187
+ return;
188
+ }
189
+ let live2 = true;
190
+ fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`).then((r) => {
191
+ if (!r.ok) warnOnce(`Config request to ${host || "this origin"} answered ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);
192
+ return r.ok ? r.json() : null;
193
+ }).then((c) => {
194
+ if (!live2) return;
195
+ if (c && Array.isArray(c.experiments)) setConfig(c);
196
+ setReady(true);
197
+ }).catch(() => {
198
+ warnOnce(`Could not reach ${host || "this origin"}. Check NEXT_PUBLIC_MRROCKET_HOST. The page keeps working with control variants.`);
199
+ if (live2) setReady(true);
200
+ });
201
+ return () => {
202
+ live2 = false;
203
+ };
204
+ }, [apiKey, host, bootstrapConfig]);
205
+ const flush = useCallback(
206
+ (beacon = false) => {
207
+ if (!queue.current.length) return;
208
+ if (!apiKey) {
209
+ queue.current.length = 0;
210
+ return;
211
+ }
212
+ const url = `${host}/api/sdk/events`;
213
+ for (const body of bodies(apiKey, queue.current.splice(0))) {
214
+ const sent = beacon && typeof navigator !== "undefined" && "sendBeacon" in navigator && navigator.sendBeacon(url, body);
215
+ if (!sent)
216
+ fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).then((r) => {
217
+ if (!r.ok) warnOnce(`Events were rejected with ${r.status}. Check the public key (Settings \u203A SDK) and NEXT_PUBLIC_MRROCKET_HOST.`);
218
+ }).catch(() => warnOnce(`Could not reach ${host || "this origin"} to send events. Check NEXT_PUBLIC_MRROCKET_HOST.`));
219
+ }
220
+ },
221
+ [apiKey, host]
222
+ );
223
+ useEffect(() => {
224
+ const timer = window.setInterval(() => flush(), 2e3);
225
+ const leave = () => flush(true);
226
+ window.addEventListener("pagehide", leave);
227
+ return () => {
228
+ window.clearInterval(timer);
229
+ window.removeEventListener("pagehide", leave);
230
+ flush(true);
231
+ };
232
+ }, [flush]);
233
+ const live = useRef({ resolved: false, tracking, distinctId, sendId, persist: false });
234
+ live.current = { resolved: visitor !== null, tracking, distinctId, sendId, persist: visitor?.mode === "cookies" };
235
+ const record = useCallback((event, props, ts = Date.now()) => {
236
+ const now = live.current;
237
+ if (!now.resolved) {
238
+ early.current.push(() => record(event, props, ts));
239
+ return;
240
+ }
241
+ if (!now.tracking) return;
242
+ const referral = carriesReferral(event) ? getReferral() : null;
243
+ queue.current.push({
244
+ event,
245
+ props: referral ? { ...props, $referral: { code: referral.code, clickId: referral.clickId } } : props,
246
+ distinctId: now.sendId,
247
+ sessionId: touchSession(now.persist),
248
+ ts,
249
+ url: window.location.href,
250
+ referrer: page.referrer,
251
+ ...page.utm,
252
+ screen: { w: window.screen.width, h: window.screen.height }
253
+ });
254
+ }, []);
255
+ const track = useCallback((event, props) => record(event, props), [record]);
256
+ const identify = useCallback(
257
+ (id, props) => {
258
+ const now = live.current;
259
+ if (!now.resolved) {
260
+ early.current.push(() => identify(id, props));
261
+ return;
262
+ }
263
+ const previous = now.sendId;
264
+ live.current = { ...now, distinctId: id, sendId: id };
265
+ record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
266
+ if (now.persist) writeCookie(ID_COOKIE, id, YEAR_S);
267
+ setIdentified(id);
268
+ setUser({ id, ...props });
269
+ },
270
+ [record]
271
+ );
272
+ useEffect(() => {
273
+ if (!visitor) return;
274
+ for (const run of early.current.splice(0)) run();
275
+ }, [visitor]);
276
+ const referralDays = config?.referral?.cookieDays ?? DEFAULT_REFERRAL_DAYS;
277
+ const storeReferrals = attribution === "always" ? effectiveConsent !== "off" : attribution === "consent" && effectiveConsent === "cookies";
278
+ const captured = useRef(null);
279
+ const storedReferral = useRef(false);
280
+ useEffect(() => {
281
+ if (!visitor || !ready || embedded) return;
282
+ if (attribution === "off" || effectiveConsent === "off") {
283
+ resetReferral();
284
+ if (attribution === "off" || storedReferral.current) forgetStoredReferral();
285
+ return;
286
+ }
287
+ const href = window.location.href;
288
+ if (captured.current === href) return;
289
+ captured.current = href;
290
+ void captureReferral({
291
+ search: window.location.search,
292
+ param: referralParam,
293
+ store: storeReferrals,
294
+ days: referralDays,
295
+ beacon: apiKey ? ({ code, clickId }) => fetch(`${host}/api/sdk/affiliate/click`, {
296
+ method: "POST",
297
+ headers: { "content-type": "application/json" },
298
+ body: JSON.stringify({ key: apiKey, code, clickId, url: href, referrer: document.referrer || void 0 }),
299
+ keepalive: true
300
+ }).then((r) => r.ok ? r.json() : null).then((b) => b?.clickId ?? null) : void 0
301
+ });
302
+ }, [visitor, ready, embedded, attribution, effectiveConsent, pathname, referralParam, storeReferrals, referralDays, apiKey, host]);
303
+ useEffect(() => {
304
+ if (!mounted) return;
305
+ const r = getReferral();
306
+ if (storeReferrals && r) storeReferral(r, referralDays);
307
+ else if (!storeReferrals && storedReferral.current) forgetStoredReferral();
308
+ storedReferral.current = storeReferrals;
309
+ }, [mounted, storeReferrals, referralDays]);
310
+ const expose = useCallback(
311
+ (key, variant) => {
312
+ if (!tracking) return;
313
+ const mark = `${pathname}|${key}`;
314
+ if (exposed.current.has(mark)) return;
315
+ exposed.current.add(mark);
316
+ track("$exposure", { experiment: key, variant });
317
+ },
318
+ [pathname, track, tracking]
319
+ );
320
+ useEffect(() => {
321
+ enterPage();
322
+ }, [pathname]);
323
+ useEffect(() => {
324
+ if (!tracking || lastPage.current === pathname) return;
325
+ lastPage.current = pathname;
326
+ track("$pageview", { path: pathname });
327
+ }, [pathname, tracking, track]);
328
+ useEffect(() => {
329
+ const onClick = (e) => {
330
+ const el = e.target?.closest("[data-ck-goal]");
331
+ if (el?.dataset.ckGoal) track(el.dataset.ckGoal, { text: el.textContent?.trim().slice(0, 80) });
332
+ };
333
+ document.addEventListener("click", onClick);
334
+ return () => document.removeEventListener("click", onClick);
335
+ }, [track]);
336
+ const texts = useMemo(() => textChanges(config, distinctId, preview), [config, distinctId, preview]);
337
+ const value = useMemo(
338
+ () => ({ apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, setConsent, texts, track, identify, expose }),
339
+ [apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, setConsent, texts, track, identify, expose]
340
+ );
341
+ return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
342
+ children,
343
+ surveys ? /* @__PURE__ */ jsx(Survey, {}) : null
344
+ ] });
345
+ };
346
+ var useCockpit = () => {
347
+ const { track, identify, distinctId, ready, embedded, consent, setConsent } = useCtx();
348
+ return { track, identify, distinctId, ready, embedded, consent, setConsent };
349
+ };
350
+ var useVariant = (key) => {
351
+ const { config, distinctId, preview, expose } = useCtx();
352
+ const variant = resolveVariant(key, config, distinctId, preview);
353
+ useEffect(() => {
354
+ if (config || preview[key]) expose(key, variant);
355
+ }, [config, preview, key, variant, expose]);
356
+ return variant;
357
+ };
358
+ var Experiment = ({ name, variants }) => {
359
+ const variant = useVariant(name);
360
+ return /* @__PURE__ */ jsx(Fragment, { children: variants[variant] ?? variants.control });
361
+ };
362
+ var Text = ({ id, children }) => {
363
+ const { texts } = useCtx();
364
+ return /* @__PURE__ */ jsx(Fragment, { children: texts[id] ?? children });
365
+ };
366
+ export {
367
+ CockpitProvider,
368
+ Experiment,
369
+ Text,
370
+ useCockpit,
371
+ useVariant
372
+ };
373
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Ctx, type CockpitContext, type CockpitUser, useCtx } from \"./context\";\nimport { Survey, forgetStoredDismissals } from \"./survey\";\nimport {\n type AttributionMode,\n DEFAULT_REFERRAL_DAYS,\n captureReferral,\n carriesReferral,\n forgetStoredReferral,\n getReferral,\n resetReferral,\n storeReferral,\n} from \"./referral\";\nimport {\n type CockpitConfig,\n type CockpitEvent,\n type ConsentMode,\n EMBED_PARAM,\n ID_COOKIE,\n PREVIEW_PARAM,\n SESSION_COOKIE,\n SESSION_TIMEOUT_MS,\n type UtmKey,\n type VariantId,\n envKey,\n newDistinctId,\n parsePreview,\n readUtm,\n resolveHost,\n resolveVariant,\n textChanges,\n} from \"./core\";\n\nconst YEAR_S = 60 * 60 * 24 * 365;\n/** The server takes at most 100 events and 64 KB per request; stay a little under the size. */\nconst MAX_BATCH = 100;\nconst MAX_BYTES = 60_000;\n\nconst readCookie = (name: string) => {\n if (typeof document === \"undefined\") return undefined;\n const raw = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`))\n ?.slice(name.length + 1);\n if (!raw) return undefined;\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n};\n\nconst writeCookie = (name: string, value: string, maxAge: number) => {\n const secure = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; samesite=lax${secure}`;\n};\n\nconst deleteCookie = (name: string) => {\n document.cookie = `${name}=; path=/; max-age=0; samesite=lax`;\n};\n\n/**\n * What lives for one page load, outside React so it survives a provider that remounts on navigation: the in-memory\n * visitor id, the session, the current page's URL and referrer, and the campaign (UTM) parameters.\n * Only touched in the browser (effects and event handlers), never during a server render.\n */\nconst page: {\n id?: string;\n session?: { id: string; last: number };\n url?: string;\n referrer?: string;\n utm: Partial<Record<UtmKey, string>>;\n} = { utm: {} };\n\nconst memoryId = () => (page.id ??= newDistinctId());\n\n/**\n * The visitor id for experiments, surveys and feedback. Only \"cookies\" reads or writes `ck_id`; the other modes keep a\n * random id in memory for this page load. The page id follows the cookie, so leaving \"cookies\" keeps the same variants.\n */\nconst visitorId = (mode: ConsentMode, write: boolean) => {\n if (mode !== \"cookies\") return memoryId();\n const stored = readCookie(ID_COOKIE);\n if (stored) return (page.id = stored);\n const id = memoryId();\n if (write) writeCookie(ID_COOKIE, id, YEAR_S);\n return id;\n};\n\n/** The session id for an event. Every event renews it; after 30 minutes without one a new session starts. */\nconst touchSession = (persist: boolean) => {\n const now = Date.now();\n const s = page.session;\n // In \"cookies\" mode the cookie wins, so tabs share a session.\n const id = (persist ? readCookie(SESSION_COOKIE) : undefined) ?? (s && now - s.last < SESSION_TIMEOUT_MS ? s.id : newDistinctId());\n page.session = { id, last: now };\n if (persist) writeCookie(SESSION_COOKIE, id, SESSION_TIMEOUT_MS / 1000);\n return id;\n};\n\n/** On every route change: the previous in-app URL becomes the referrer, and a URL with UTM parameters replaces the old ones. */\nconst enterPage = () => {\n const href = window.location.href;\n if (page.url === href) return;\n page.referrer = page.url ?? (document.referrer || undefined);\n page.url = href;\n const utm = readUtm(window.location.search);\n if (Object.keys(utm).length) page.utm = utm;\n};\n\n/** Do Not Track or Global Privacy Control. */\nconst privacySignal = () => {\n const nav = navigator as Navigator & { doNotTrack?: string | null; globalPrivacyControl?: boolean };\n const win = window as unknown as { doNotTrack?: string | null };\n return nav.doNotTrack === \"1\" || win.doNotTrack === \"1\" || nav.globalPrivacyControl === true;\n};\n\n/** Tells the developer once, in the browser console, why the SDK is not reaching MRRocket (a wrong host or key is the usual cause). */\nconst warned = new Set<string>();\nconst warnOnce = (message: string) => {\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(`[MRRocket] ${message}`);\n};\n\nconst byteLength = (s: string) => new TextEncoder().encode(s).length;\n\n/** Request bodies of at most 100 events each, under the size limit. An event too large on its own is dropped. */\nconst bodies = (key: string, events: CockpitEvent[]) => {\n const head = `{\"key\":${JSON.stringify(key)},\"events\":[`;\n const empty = byteLength(head) + 2;\n const out: string[] = [];\n let chunk: string[] = [];\n let size = empty;\n const close = () => {\n if (chunk.length) out.push(`${head}${chunk.join(\",\")}]}`);\n chunk = [];\n size = empty;\n };\n for (const event of events) {\n const json = JSON.stringify(event);\n const n = byteLength(json) + 1;\n if (empty + n > MAX_BYTES) continue;\n if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();\n chunk.push(json);\n size += n;\n }\n close();\n return out;\n};\n\nexport interface CockpitProviderProps {\n /** The product's public key from Cockpit › Settings › SDK. Safe in the browser. Default: `NEXT_PUBLIC_MRROCKET_KEY`. */\n apiKey?: string;\n /** Where MRRocket runs, like `https://app.example.com`. Default: `NEXT_PUBLIC_MRROCKET_HOST`, else the page's own origin. */\n host?: string;\n preview?: Record<string, VariantId>;\n embedded?: boolean;\n /** Config fetched on the server, to render the right variant on the first paint. */\n config?: CockpitConfig;\n /** Your own id for this visitor, for example a signed-in user id. Keeps variants stable without cookies. */\n distinctId?: string;\n /**\n * \"cookieless\" (default) stores nothing in the browser, \"cookies\" keeps a visitor cookie for a year, \"off\" sends\n * nothing. The SDK does not remember the choice: set it on every page load, from the prop or setConsent().\n */\n consent?: ConsentMode;\n /** Send nothing when the browser asks not to be tracked (Do Not Track or Global Privacy Control). */\n respectDnt?: boolean;\n /** Ask visitors the surveys you run in Cockpit. Set false to switch them off. */\n surveys?: boolean;\n /**\n * Affiliate referrals (`?ref=` links and MRRocket's tracked links). \"consent\" (default): stored in the first-party\n * `mrr_ref` cookie only while consent is \"cookies\", in memory otherwise. \"always\": stored whatever the consent (only\n * where your legal basis allows it). \"off\": referrals are ignored.\n */\n attribution?: AttributionMode;\n /** The query parameter of direct partner links, default `ref` (`https://your.site/?ref=maya`). */\n referralParam?: string;\n children: ReactNode;\n}\n\n/**\n * Put it once around your app, in `app/layout.tsx`. It loads your experiments, keeps each visitor in one variant,\n * records page views and sends your events to Cockpit in small batches.\n *\n * `preview` forces variants for this render, for example from the `ck_preview` search param on the server,\n * so a Cockpit preview never flickers. Without it the provider reads the param itself after the first render.\n */\nexport const CockpitProvider = ({\n apiKey: apiKeyProp,\n host: hostProp,\n preview: forced,\n embedded: embeddedProp,\n config: bootstrapConfig,\n distinctId: bootstrapId,\n consent: consentProp = \"cookieless\",\n respectDnt = false,\n surveys = true,\n attribution = \"consent\",\n referralParam = \"ref\",\n children,\n}: CockpitProviderProps) => {\n const pathname = usePathname();\n const apiKey = apiKeyProp ?? envKey() ?? \"\";\n const host = resolveHost(hostProp);\n const [config, setConfig] = useState<CockpitConfig | null>(bootstrapConfig ?? null);\n const [consent, setConsentState] = useState<ConsentMode>(consentProp);\n const [mounted, setMounted] = useState(false);\n const [dnt, setDnt] = useState(false);\n // The visitor id and the consent mode it was made for; null until the browser has been read.\n const [visitor, setVisitor] = useState<{ mode: ConsentMode; id: string } | null>(null);\n const [identified, setIdentified] = useState<string | undefined>();\n const [urlPreview, setUrlPreview] = useState<Record<string, VariantId>>({});\n const [urlEmbedded, setUrlEmbedded] = useState(false);\n const [user, setUser] = useState<CockpitUser | undefined>();\n const [ready, setReady] = useState(!!bootstrapConfig);\n const queue = useRef<CockpitEvent[]>([]);\n // Calls from before the visitor was known (a child's effect runs before ours); replayed once it is.\n const early = useRef<(() => void)[]>([]);\n const exposed = useRef(new Set<string>());\n const lastPage = useRef<string | null>(null);\n\n const bootstrap = bootstrapId && bootstrapId !== \"anonymous\" ? bootstrapId : undefined;\n const preview = useMemo(() => ({ ...urlPreview, ...forced }), [urlPreview, forced]);\n const embedded = embeddedProp ?? urlEmbedded;\n const effectiveConsent: ConsentMode = respectDnt && dnt ? \"off\" : consent;\n const distinctId = identified ?? bootstrap ?? visitor?.id ?? \"anonymous\";\n const tracking = !embedded && effectiveConsent !== \"off\" && visitor?.mode === effectiveConsent;\n // Without cookies, events carry an id only after identify(); the server counts everyone else by its daily hash.\n const sendId = effectiveConsent === \"cookies\" ? distinctId : identified;\n\n // Preview flags and the browser's privacy signal, once, in the browser.\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n setUrlPreview(parsePreview(params.get(PREVIEW_PARAM)));\n setUrlEmbedded(params.get(EMBED_PARAM) === \"1\");\n setDnt(privacySignal());\n setMounted(true);\n }, []);\n\n const setConsent = useCallback((mode: ConsentMode) => {\n if (mode === \"off\") queue.current.length = 0;\n setConsentState(mode);\n }, []);\n\n // Follow the `consent` prop when it changes; setConsent() calls in between stay until it does.\n const lastProp = useRef(consentProp);\n useEffect(() => {\n if (lastProp.current === consentProp) return;\n lastProp.current = consentProp;\n setConsent(consentProp);\n }, [consentProp, setConsent]);\n\n // Storage follows consent. Leaving \"cookies\" deletes what the SDK stored; this page keeps its id in memory.\n const storedFor = useRef<ConsentMode | null>(null);\n useEffect(() => {\n if (!mounted) return;\n const before = storedFor.current;\n storedFor.current = effectiveConsent;\n if (before === \"cookies\" && effectiveConsent !== \"cookies\") {\n deleteCookie(ID_COOKIE);\n deleteCookie(SESSION_COOKIE);\n forgetStoredDismissals();\n }\n if (effectiveConsent === \"off\") {\n queue.current.length = 0;\n page.session = undefined;\n }\n setVisitor({ mode: effectiveConsent, id: visitorId(effectiveConsent, !bootstrap) });\n }, [mounted, effectiveConsent, bootstrap]);\n\n // Experiments for this key. Without a key or when MRRocket is unreachable the page keeps control and carries on.\n useEffect(() => {\n if (!apiKey) warnOnce(\"No public key. Pass apiKey or set NEXT_PUBLIC_MRROCKET_KEY; nothing is tracked until then.\");\n if (bootstrapConfig || !apiKey) {\n setReady(true);\n return;\n }\n let live = true;\n fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`)\n .then((r) => {\n if (!r.ok) warnOnce(`Config request to ${host || \"this origin\"} answered ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);\n return r.ok ? r.json() : null;\n })\n .then((c: CockpitConfig | null) => {\n if (!live) return;\n if (c && Array.isArray(c.experiments)) setConfig(c);\n setReady(true);\n })\n .catch(() => {\n warnOnce(`Could not reach ${host || \"this origin\"}. Check NEXT_PUBLIC_MRROCKET_HOST. The page keeps working with control variants.`);\n if (live) setReady(true);\n });\n return () => {\n live = false;\n };\n }, [apiKey, host, bootstrapConfig]);\n\n const flush = useCallback(\n (beacon = false) => {\n if (!queue.current.length) return;\n if (!apiKey) {\n queue.current.length = 0;\n return;\n }\n const url = `${host}/api/sdk/events`;\n for (const body of bodies(apiKey, queue.current.splice(0))) {\n const sent = beacon && typeof navigator !== \"undefined\" && \"sendBeacon\" in navigator && navigator.sendBeacon(url, body);\n if (!sent)\n fetch(url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, keepalive: true })\n .then((r) => {\n if (!r.ok) warnOnce(`Events were rejected with ${r.status}. Check the public key (Settings › SDK) and NEXT_PUBLIC_MRROCKET_HOST.`);\n })\n .catch(() => warnOnce(`Could not reach ${host || \"this origin\"} to send events. Check NEXT_PUBLIC_MRROCKET_HOST.`));\n }\n },\n [apiKey, host],\n );\n\n useEffect(() => {\n const timer = window.setInterval(() => flush(), 2000);\n const leave = () => flush(true);\n window.addEventListener(\"pagehide\", leave);\n return () => {\n window.clearInterval(timer);\n window.removeEventListener(\"pagehide\", leave);\n flush(true);\n };\n }, [flush]);\n\n // The latest values for callbacks that must not change identity, so effects that use them run once.\n const live = useRef({ resolved: false, tracking, distinctId, sendId, persist: false });\n live.current = { resolved: visitor !== null, tracking, distinctId, sendId, persist: visitor?.mode === \"cookies\" };\n\n const record = useCallback((event: string, props: Record<string, unknown> | undefined, ts = Date.now()) => {\n const now = live.current;\n if (!now.resolved) {\n early.current.push(() => record(event, props, ts));\n return;\n }\n // Previews in Cockpit never count, and nothing is sent without consent.\n if (!now.tracking) return;\n const referral = carriesReferral(event) ? getReferral() : null;\n queue.current.push({\n event,\n props: referral ? { ...props, $referral: { code: referral.code, clickId: referral.clickId } } : props,\n distinctId: now.sendId,\n sessionId: touchSession(now.persist),\n ts,\n url: window.location.href,\n referrer: page.referrer,\n ...page.utm,\n screen: { w: window.screen.width, h: window.screen.height },\n });\n }, []);\n\n const track = useCallback((event: string, props?: Record<string, unknown>) => record(event, props), [record]);\n\n const identify = useCallback(\n (id: string, props?: Record<string, unknown>) => {\n const now = live.current;\n if (!now.resolved) {\n early.current.push(() => identify(id, props));\n return;\n }\n const previous = now.sendId;\n // From here on events carry this id in every mode; only \"cookies\" keeps it for the next visit.\n live.current = { ...now, distinctId: id, sendId: id };\n record(\"$identify\", { ...props, id, ...(previous && previous !== id ? { previousId: previous } : {}) });\n if (now.persist) writeCookie(ID_COOKIE, id, YEAR_S);\n setIdentified(id);\n setUser({ id, ...(props as Omit<CockpitUser, \"id\">) });\n },\n [record],\n );\n\n // Replay what came in before the visitor was known: sent when tracking is on, dropped when it is not.\n useEffect(() => {\n if (!visitor) return;\n for (const run of early.current.splice(0)) run();\n }, [visitor]);\n\n // Affiliate referrals: read once the consent is known and the program's cookie days have loaded.\n const referralDays = config?.referral?.cookieDays ?? DEFAULT_REFERRAL_DAYS;\n const storeReferrals = attribution === \"always\" ? effectiveConsent !== \"off\" : attribution === \"consent\" && effectiveConsent === \"cookies\";\n const captured = useRef<string | null>(null);\n const storedReferral = useRef(false);\n useEffect(() => {\n if (!visitor || !ready || embedded) return;\n if (attribution === \"off\" || effectiveConsent === \"off\") {\n resetReferral();\n if (attribution === \"off\" || storedReferral.current) forgetStoredReferral();\n return;\n }\n const href = window.location.href;\n if (captured.current === href) return;\n captured.current = href;\n void captureReferral({\n search: window.location.search,\n param: referralParam,\n store: storeReferrals,\n days: referralDays,\n beacon: apiKey\n ? ({ code, clickId }) =>\n fetch(`${host}/api/sdk/affiliate/click`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ key: apiKey, code, clickId, url: href, referrer: document.referrer || undefined }),\n keepalive: true,\n })\n .then((r) => (r.ok ? (r.json() as Promise<{ clickId?: string | null }>) : null))\n .then((b) => b?.clickId ?? null)\n : undefined,\n });\n }, [visitor, ready, embedded, attribution, effectiveConsent, pathname, referralParam, storeReferrals, referralDays, apiKey, host]);\n\n // Consent given later keeps this page load's referral; consent withdrawn deletes what was stored.\n useEffect(() => {\n if (!mounted) return;\n const r = getReferral();\n if (storeReferrals && r) storeReferral(r, referralDays);\n else if (!storeReferrals && storedReferral.current) forgetStoredReferral();\n storedReferral.current = storeReferrals;\n }, [mounted, storeReferrals, referralDays]);\n\n // `tracking` is a dependency so that variants seen before consent are recorded once tracking starts.\n const expose = useCallback(\n (key: string, variant: VariantId) => {\n if (!tracking) return;\n const mark = `${pathname}|${key}`;\n if (exposed.current.has(mark)) return;\n exposed.current.add(mark);\n track(\"$exposure\", { experiment: key, variant });\n },\n [pathname, track, tracking],\n );\n\n // The referrer and campaign of each page; before the page view below, so it already carries them.\n useEffect(() => {\n enterPage();\n }, [pathname]);\n\n // One page view per route change while tracking, so a later setConsent() records the current page right away\n // (a ref keeps React's dev double-run from counting twice).\n useEffect(() => {\n if (!tracking || lastPage.current === pathname) return;\n lastPage.current = pathname;\n // Exposures are remembered per path (\"/pricing|test\"), so nothing needs clearing here.\n track(\"$pageview\", { path: pathname });\n }, [pathname, tracking, track]);\n\n // Clicks on anything marked `data-ck-goal=\"signup_started\"` count as that goal.\n useEffect(() => {\n const onClick = (e: MouseEvent) => {\n const el = (e.target as HTMLElement | null)?.closest<HTMLElement>(\"[data-ck-goal]\");\n if (el?.dataset.ckGoal) track(el.dataset.ckGoal, { text: el.textContent?.trim().slice(0, 80) });\n };\n document.addEventListener(\"click\", onClick);\n return () => document.removeEventListener(\"click\", onClick);\n }, [track]);\n\n const texts = useMemo(() => textChanges(config, distinctId, preview), [config, distinctId, preview]);\n\n const value = useMemo<CockpitContext>(\n () => ({ apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, setConsent, texts, track, identify, expose }),\n [apiKey, host, user, ready, distinctId, config, preview, embedded, consent, effectiveConsent, setConsent, texts, track, identify, expose],\n );\n return (\n <Ctx.Provider value={value}>\n {children}\n {surveys ? <Survey /> : null}\n </Ctx.Provider>\n );\n};\n\n/** `track`, `identify`, the consent mode and its setter, the visitor id and whether the experiments have loaded. */\nexport const useCockpit = () => {\n const { track, identify, distinctId, ready, embedded, consent, setConsent } = useCtx();\n return { track, identify, distinctId, ready, embedded, consent, setConsent };\n};\n\n/** The variant this visitor sees, for example `\"control\"` or `\"a\"`. Records the exposure. */\nexport const useVariant = (key: string): VariantId => {\n const { config, distinctId, preview, expose } = useCtx();\n const variant = resolveVariant(key, config, distinctId, preview);\n useEffect(() => {\n if (config || preview[key]) expose(key, variant);\n }, [config, preview, key, variant, expose]);\n return variant;\n};\n\n/** Renders the element for the visitor's variant; control when the variant has no element. */\nexport const Experiment = ({ name, variants }: { name: string; variants: Record<VariantId, ReactNode> & { control: ReactNode } }) => {\n const variant = useVariant(name);\n return <>{variants[variant] ?? variants.control}</>;\n};\n\n/**\n * A text that experiments may change without a deploy: Cockpit sends the new copy for `id` and this renders it.\n * Write your default as the children.\n */\nexport const Text = ({ id, children }: { id: string; children: string }) => {\n const { texts } = useCtx();\n return <>{texts[id] ?? children}</>;\n};\n"],"mappings":"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","names":["live"]}
@@ -0,0 +1,31 @@
1
+ import type { CockpitConfig, ConsentMode, VariantId } from "./core.js";
2
+ /** Who the visitor is, from identify(). Sent along with feedback and survey answers. */
3
+ export interface CockpitUser {
4
+ id: string;
5
+ name?: string;
6
+ email?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface CockpitContext {
10
+ apiKey: string;
11
+ host: string;
12
+ user?: CockpitUser;
13
+ ready: boolean;
14
+ distinctId: string;
15
+ config: CockpitConfig | null;
16
+ preview: Record<string, VariantId>;
17
+ /** Embedded in Cockpit as a preview: nothing is tracked. */
18
+ embedded: boolean;
19
+ /** The consent mode the site set, with the `consent` prop or setConsent(). */
20
+ consent: ConsentMode;
21
+ /** What applies right now: "off" when the browser sends Do Not Track and the provider respects it. */
22
+ effectiveConsent: ConsentMode;
23
+ setConsent: (mode: ConsentMode) => void;
24
+ texts: Record<string, string>;
25
+ track: (event: string, props?: Record<string, unknown>) => void;
26
+ identify: (id: string, props?: Record<string, unknown>) => void;
27
+ /** Records that the visitor saw a variant, once per experiment and page view. */
28
+ expose: (key: string, variant: VariantId) => void;
29
+ }
30
+ export declare const Ctx: import("react").Context<CockpitContext | null>;
31
+ export declare const useCtx: () => CockpitContext;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ // src/context.ts
4
+ import { createContext, useContext } from "react";
5
+ var Ctx = createContext(null);
6
+ var useCtx = () => {
7
+ const c = useContext(Ctx);
8
+ if (!c) throw new Error("Cockpit: wrap your app in <CockpitProvider apiKey=\u2026>.");
9
+ return c;
10
+ };
11
+ export {
12
+ Ctx,
13
+ useCtx
14
+ };
15
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { CockpitConfig, ConsentMode, VariantId } from \"./core\";\n\n/** Who the visitor is, from identify(). Sent along with feedback and survey answers. */\nexport interface CockpitUser {\n id: string;\n name?: string;\n email?: string;\n [key: string]: unknown;\n}\n\nexport interface CockpitContext {\n apiKey: string;\n host: string;\n user?: CockpitUser;\n ready: boolean;\n distinctId: string;\n config: CockpitConfig | null;\n preview: Record<string, VariantId>;\n /** Embedded in Cockpit as a preview: nothing is tracked. */\n embedded: boolean;\n /** The consent mode the site set, with the `consent` prop or setConsent(). */\n consent: ConsentMode;\n /** What applies right now: \"off\" when the browser sends Do Not Track and the provider respects it. */\n effectiveConsent: ConsentMode;\n setConsent: (mode: ConsentMode) => void;\n texts: Record<string, string>;\n track: (event: string, props?: Record<string, unknown>) => void;\n identify: (id: string, props?: Record<string, unknown>) => void;\n /** Records that the visitor saw a variant, once per experiment and page view. */\n expose: (key: string, variant: VariantId) => void;\n}\n\nexport const Ctx = createContext<CockpitContext | null>(null);\n\nexport const useCtx = () => {\n const c = useContext(Ctx);\n if (!c) throw new Error(\"Cockpit: wrap your app in <CockpitProvider apiKey=…>.\");\n return c;\n};\n"],"mappings":";;;AAEA,SAAS,eAAe,kBAAkB;AAiCnC,IAAM,MAAM,cAAqC,IAAI;AAErD,IAAM,SAAS,MAAM;AACxB,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAG,OAAM,IAAI,MAAM,4DAAuD;AAC/E,SAAO;AACX;","names":[]}
package/dist/core.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.
3
+ * No React, no browser APIs, so the server helper, the proxy helper and the client share it.
4
+ */
5
+ export type VariantId = string;
6
+ /** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */
7
+ export interface VariantConfig {
8
+ id: VariantId;
9
+ /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */
10
+ weight: number;
11
+ changes?: Record<string, string>;
12
+ }
13
+ export interface ExperimentConfig {
14
+ /** The experiment's key, the same as in Cockpit. */
15
+ key: string;
16
+ status: "running" | "draft" | "stopped";
17
+ /** Control first. */
18
+ variants: VariantConfig[];
19
+ /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */
20
+ winner?: VariantId;
21
+ }
22
+ export interface CockpitConfig {
23
+ /** The product the public key belongs to. */
24
+ project: string;
25
+ experiments: ExperimentConfig[];
26
+ /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */
27
+ referral?: {
28
+ cookieDays: number;
29
+ };
30
+ }
31
+ /**
32
+ * What the SDK may send and store.
33
+ * - "cookieless" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors
34
+ * with a hash of key, date, IP and user agent that changes every day.
35
+ * - "cookies": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.
36
+ * - "off": nothing is sent and nothing is stored.
37
+ */
38
+ export type ConsentMode = "cookieless" | "cookies" | "off";
39
+ /** The campaign parameters the SDK reads from the landing URL. */
40
+ export declare const UTM_KEYS: readonly ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
41
+ export type UtmKey = (typeof UTM_KEYS)[number];
42
+ /** One event as it goes over the wire to `POST /api/sdk/events`. */
43
+ export interface CockpitEvent {
44
+ event: string;
45
+ /** The visitor. Missing in "cookieless" mode until identify(): the server then uses its daily visitor hash. */
46
+ distinctId?: string;
47
+ /** Ends after 30 minutes without an event. */
48
+ sessionId?: string;
49
+ props?: Record<string, unknown>;
50
+ /** Milliseconds since the epoch. */
51
+ ts: number;
52
+ url?: string;
53
+ /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */
54
+ referrer?: string;
55
+ utm_source?: string;
56
+ utm_medium?: string;
57
+ utm_campaign?: string;
58
+ utm_term?: string;
59
+ utm_content?: string;
60
+ /** The screen size in CSS pixels. */
61
+ screen?: {
62
+ w: number;
63
+ h: number;
64
+ };
65
+ }
66
+ /** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */
67
+ export declare const PREVIEW_PARAM = "ck_preview";
68
+ /** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */
69
+ export declare const EMBED_PARAM = "ck_embed";
70
+ /** The cookie that keeps a visitor in the same variant across visits. Only written in "cookies" mode. */
71
+ export declare const ID_COOKIE = "ck_id";
72
+ /** The cookie that holds the session id in "cookies" mode. */
73
+ export declare const SESSION_COOKIE = "ck_sid";
74
+ /** A session ends after this long without an event. */
75
+ export declare const SESSION_TIMEOUT_MS: number;
76
+ /** The UTM parameters of a query string, like `"?utm_source=x"`. Empty values are left out. */
77
+ export declare const readUtm: (search: string) => Partial<Record<UtmKey, string>>;
78
+ /** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */
79
+ export declare const hashUnit: (input: string) => number;
80
+ /** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */
81
+ export declare const assign: (experiment: ExperimentConfig, distinctId: string) => VariantId;
82
+ /** `"a:b,c:d"` → `{ a: "b", c: "d" }`. Ignores anything malformed. */
83
+ export declare const parsePreview: (value: string | string[] | null | undefined) => Record<string, VariantId>;
84
+ /** A link that shows one variant of a page, for example in Cockpit's preview frames. */
85
+ export declare const previewHref: (url: string, forced: Record<string, VariantId>, embed?: boolean) => string;
86
+ /** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */
87
+ export declare const resolveVariant: (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => VariantId;
88
+ /**
89
+ * Every text change that applies to this visitor, merged in config order. Control keeps the page's own words,
90
+ * so only a non-control variant (assigned, forced in a preview, or a shipped winner) changes text.
91
+ */
92
+ export declare const textChanges: (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => Record<string, string>;
93
+ /**
94
+ * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js
95
+ * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on
96
+ * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.
97
+ */
98
+ export declare const envHost: () => string | undefined;
99
+ /** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */
100
+ export declare const envKey: () => string | undefined;
101
+ /**
102
+ * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else "" (the page's own origin, which is how
103
+ * MRRocket's own demo pages call it). Without a trailing slash.
104
+ */
105
+ export declare const resolveHost: (host?: string) => string;
106
+ /** A random visitor id. */
107
+ export declare const newDistinctId: () => string;