@mrrocket/sdk 0.21.0 → 0.23.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.
@@ -43,6 +43,7 @@ __export(native_exports, {
43
43
  useMessenger: () => useMessenger,
44
44
  useNavigationTracking: () => useNavigationTracking,
45
45
  useOnboarding: () => useOnboarding,
46
+ useOnboardingReport: () => useOnboardingReport,
46
47
  usePaywall: () => usePaywall,
47
48
  usePush: () => usePush,
48
49
  useReview: () => useReview,
@@ -113,7 +114,7 @@ var onLocale = (fn) => {
113
114
  };
114
115
 
115
116
  // src/core.ts
116
- var SDK_VERSION = "0.21.0";
117
+ var SDK_VERSION = "0.23.0";
117
118
  var propOf = (person, name) => {
118
119
  const want = name.trim().toLowerCase();
119
120
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -155,6 +156,7 @@ var flagFor = (key, config, distinctId, person = {}) => {
155
156
  const flag = config?.flags?.find((f) => f.key === key);
156
157
  return flag ? evaluateFlag(flag, distinctId, person) : false;
157
158
  };
159
+ var CONSENT_OFF_EVENT = "$consent_off";
158
160
  var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
159
161
  var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
160
162
  var utmFrom = (get) => {
@@ -237,212 +239,6 @@ var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypt
237
239
 
238
240
  // src/onboarding-state.ts
239
241
  var import_react = require("react");
240
- var none = [];
241
- var useOnboardingItem = (flows, id, start) => {
242
- const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
243
- (0, import_react.useEffect)(() => {
244
- if (!flows || !id) {
245
- setState(null);
246
- return;
247
- }
248
- const up = () => {
249
- const s2 = flows.onboarding.item(id);
250
- setState(s2);
251
- if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
252
- };
253
- up();
254
- return flows.subscribe(up);
255
- }, [flows, id]);
256
- return (0, import_react.useMemo)(
257
- () => ({
258
- item: state?.item ?? null,
259
- steps: state?.steps ?? none,
260
- done: !!state?.done,
261
- dismissed: !!state?.dismissed,
262
- eligible: !!state?.eligible,
263
- complete: (stepId) => {
264
- if (flows && id) flows.onboarding.complete(id, stepId);
265
- },
266
- dismiss: () => {
267
- if (flows && id) flows.onboarding.dismissItem(id);
268
- },
269
- answer: (question, value) => {
270
- if (flows && id) flows.onboarding.answerItem(id, question, value);
271
- },
272
- start
273
- }),
274
- [state, flows, id, start]
275
- );
276
- };
277
-
278
- // src/identity.ts
279
- var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
280
- var warnUnsigned = (status, log = (m) => console.warn(m)) => {
281
- const g2 = globalThis;
282
- if (status !== 401 || g2.__mrrocketUnsigned) return;
283
- g2.__mrrocketUnsigned = true;
284
- log(`[MRRocket] ${UNSIGNED_HELP}`);
285
- };
286
- var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
287
- const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
288
- if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
289
- if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
290
- return signed && signed.id === id ? signed : null;
291
- };
292
- var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
293
- var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
294
-
295
- // src/api-client.ts
296
- var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
297
- var CHAT_FILES_PER_MESSAGE = 4;
298
- var CORNERS = {
299
- round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
300
- soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
301
- square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
302
- };
303
- var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
304
- var inkOn = (hex) => {
305
- const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
306
- if (!m) return "#ffffff";
307
- const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
308
- const c = parseInt(x, 16) / 255;
309
- return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
310
- });
311
- return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
312
- };
313
- var ApiError = class extends Error {
314
- constructor(message, status) {
315
- super(message);
316
- __publicField(this, "status");
317
- this.status = status;
318
- }
319
- };
320
- var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
321
- var withLang = (url) => {
322
- const q = qs({});
323
- return q ? `${url}?${q}` : url;
324
- };
325
- var call = async (url, init, signal) => {
326
- let res;
327
- try {
328
- res = await fetch(url, {
329
- ...init,
330
- signal,
331
- // A form (a file upload) sets its own multipart type with the boundary.
332
- headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
333
- });
334
- } catch (e) {
335
- if (e.name === "AbortError") throw e;
336
- throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
337
- }
338
- if (!res.ok) {
339
- warnUnsigned(res.status);
340
- let detail = "";
341
- try {
342
- detail = (await res.json()).error ?? "";
343
- } catch {
344
- }
345
- throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
346
- }
347
- return await res.json();
348
- };
349
- var createApi = (c) => {
350
- const base = `${c.host}/api/sdk`;
351
- const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
352
- const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
353
- const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
354
- return {
355
- list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
356
- post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
357
- roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
358
- changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
359
- submit: (p) => post("/feedback", p),
360
- vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
361
- comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
362
- comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
363
- survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
364
- seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
365
- respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
366
- /** One chat by id, else the visitor's newest; `conversations` lists them all. */
367
- chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
368
- /**
369
- * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
370
- * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
371
- */
372
- sendChat: (m) => post("/messages", m),
373
- /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
374
- uploadChatFile: (file, signal) => {
375
- const form = new FormData();
376
- form.append("key", c.apiKey);
377
- form.append("distinctId", c.distinctId);
378
- if (c.userHash) form.append("userHash", c.userHash);
379
- if ("uri" in file) form.append("file", file);
380
- else form.append("file", file, file.name || "file");
381
- return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
382
- },
383
- /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
384
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
385
- articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
386
- article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
387
- messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
388
- rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
389
- };
390
- };
391
-
392
- // src/batch.ts
393
- var MAX_BATCH = 100;
394
- var MAX_BYTES = 6e4;
395
- var byteLength = (s2) => {
396
- let n = 0;
397
- for (let i = 0; i < s2.length; i++) {
398
- const c = s2.charCodeAt(i);
399
- if (c < 128) n += 1;
400
- else if (c < 2048) n += 2;
401
- else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
402
- n += 4;
403
- i++;
404
- } else n += 3;
405
- }
406
- return n;
407
- };
408
- var batchRequests = (key, events, sdk, userHash) => {
409
- const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
410
- const empty = byteLength(head) + 2;
411
- const out = [];
412
- let chunk = [];
413
- let items = [];
414
- let size = empty;
415
- const close = () => {
416
- if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
417
- chunk = [];
418
- items = [];
419
- size = empty;
420
- };
421
- for (const event of events) {
422
- const json = JSON.stringify(event);
423
- const n = byteLength(json) + 1;
424
- if (empty + n > MAX_BYTES) continue;
425
- if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
426
- chunk.push(json);
427
- items.push(event);
428
- size += n;
429
- }
430
- close();
431
- return out;
432
- };
433
-
434
- // src/errors.ts
435
- var MAX_STACK = 8 * 1024;
436
- var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
437
- var describeError = (error) => {
438
- 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 };
439
- if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
440
- try {
441
- return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
442
- } catch {
443
- return { type: "Error", message: String(error) };
444
- }
445
- };
446
242
 
447
243
  // src/flows.ts
448
244
  var CANCEL_EVENT = "$cancel_flow";
@@ -1303,6 +1099,245 @@ var createFlows = (env) => {
1303
1099
  return controller;
1304
1100
  };
1305
1101
 
1102
+ // src/onboarding-report.ts
1103
+ var KIND = { questions: "question", checklist: "checklist", tour: "tour" };
1104
+ var createOnboardingReport = (track, name, options = {}) => {
1105
+ const type = options.type ?? "questions";
1106
+ const kind = KIND[type] ?? "question";
1107
+ const seen = /* @__PURE__ */ new Set();
1108
+ const send = (props) => track(ONBOARDING_EVENT, { code: eventSlug(name), name, type, ...options.order?.length ? { order: options.order } : {}, ...props });
1109
+ return {
1110
+ saw: (step, title) => {
1111
+ if (seen.has(step)) return;
1112
+ seen.add(step);
1113
+ send({ step, title, kind, action: "shown" });
1114
+ },
1115
+ answered: (question, answer, label) => send({ step: question, kind: "question", action: "completed", question, answer, label: label ?? answer }),
1116
+ done: (step) => send({ step, kind, action: "completed" }),
1117
+ closed: (step) => send({ ...step ? { step } : {}, kind, action: "dismissed" })
1118
+ };
1119
+ };
1120
+
1121
+ // src/onboarding-state.ts
1122
+ var none = [];
1123
+ var useOnboardingItem = (flows, id, start) => {
1124
+ const [state, setState] = (0, import_react.useState)(() => flows && id ? flows.onboarding.item(id) : null);
1125
+ (0, import_react.useEffect)(() => {
1126
+ if (!flows || !id) {
1127
+ setState(null);
1128
+ return;
1129
+ }
1130
+ const up = () => {
1131
+ const s2 = flows.onboarding.item(id);
1132
+ setState(s2);
1133
+ if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
1134
+ };
1135
+ up();
1136
+ return flows.subscribe(up);
1137
+ }, [flows, id]);
1138
+ return (0, import_react.useMemo)(
1139
+ () => ({
1140
+ item: state?.item ?? null,
1141
+ steps: state?.steps ?? none,
1142
+ done: !!state?.done,
1143
+ dismissed: !!state?.dismissed,
1144
+ eligible: !!state?.eligible,
1145
+ complete: (stepId) => {
1146
+ if (flows && id) flows.onboarding.complete(id, stepId);
1147
+ },
1148
+ dismiss: () => {
1149
+ if (flows && id) flows.onboarding.dismissItem(id);
1150
+ },
1151
+ answer: (question, value) => {
1152
+ if (flows && id) flows.onboarding.answerItem(id, question, value);
1153
+ },
1154
+ start
1155
+ }),
1156
+ [state, flows, id, start]
1157
+ );
1158
+ };
1159
+ var useReportWith = (track, name, options) => {
1160
+ const key = JSON.stringify(options ?? null);
1161
+ return (0, import_react.useMemo)(() => createOnboardingReport(track, name, options), [track, name, key]);
1162
+ };
1163
+
1164
+ // src/identity.ts
1165
+ var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
1166
+ var warnUnsigned = (status, log = (m) => console.warn(m)) => {
1167
+ const g2 = globalThis;
1168
+ if (status !== 401 || g2.__mrrocketUnsigned) return;
1169
+ g2.__mrrocketUnsigned = true;
1170
+ log(`[MRRocket] ${UNSIGNED_HELP}`);
1171
+ };
1172
+ var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
1173
+ const h3 = typeof hash === "string" ? hash.trim().toLowerCase() : "";
1174
+ if (/^[0-9a-f]{64}$/.test(h3)) return { id, hash: h3 };
1175
+ if (h3) log("[MRRocket] identify: that hash is no userHash(id); not sent.");
1176
+ return signed && signed.id === id ? signed : null;
1177
+ };
1178
+ var hashFor = (signed, id) => signed && id && signed.id === id ? signed.hash : void 0;
1179
+ var hashForAll = (signed, ids) => signed && ids.some((id) => id === signed.id) && ids.every((id) => !id || id === signed.id) ? signed.hash : void 0;
1180
+
1181
+ // src/api-client.ts
1182
+ var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
1183
+ var CHAT_FILES_PER_MESSAGE = 4;
1184
+ var CORNERS = {
1185
+ round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
1186
+ soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
1187
+ square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
1188
+ };
1189
+ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
1190
+ var inkOn = (hex) => {
1191
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
1192
+ if (!m) return "#ffffff";
1193
+ const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
1194
+ const c = parseInt(x, 16) / 255;
1195
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
1196
+ });
1197
+ return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
1198
+ };
1199
+ var stillThere = (t2, chats) => (m) => {
1200
+ if (m.chat === t2.conversation?.id) return t2.messages.some((x) => x.id === m.id);
1201
+ const last = chats.find((c) => c.id === m.chat)?.last;
1202
+ return !!last && m.at <= last.at;
1203
+ };
1204
+ var ApiError = class extends Error {
1205
+ constructor(message, status) {
1206
+ super(message);
1207
+ __publicField(this, "status");
1208
+ this.status = status;
1209
+ }
1210
+ };
1211
+ var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1212
+ var withLang = (url) => {
1213
+ const q = qs({});
1214
+ return q ? `${url}?${q}` : url;
1215
+ };
1216
+ var call = async (url, init, signal) => {
1217
+ let res;
1218
+ try {
1219
+ res = await fetch(url, {
1220
+ ...init,
1221
+ signal,
1222
+ // A form (a file upload) sets its own multipart type with the boundary.
1223
+ headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
1224
+ });
1225
+ } catch (e) {
1226
+ if (e.name === "AbortError") throw e;
1227
+ throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
1228
+ }
1229
+ if (!res.ok) {
1230
+ warnUnsigned(res.status);
1231
+ let detail = "";
1232
+ try {
1233
+ detail = (await res.json()).error ?? "";
1234
+ } catch {
1235
+ }
1236
+ throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
1237
+ }
1238
+ return await res.json();
1239
+ };
1240
+ var createApi = (c) => {
1241
+ const base = `${c.host}/api/sdk`;
1242
+ const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
1243
+ const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
1244
+ const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
1245
+ return {
1246
+ list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
1247
+ post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1248
+ roadmap: (signal) => call(`${base}/feedback/roadmap?${qs(me)}`, void 0, signal),
1249
+ changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
1250
+ submit: (p) => post("/feedback", p),
1251
+ vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
1252
+ comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
1253
+ comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
1254
+ survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
1255
+ seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
1256
+ respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
1257
+ /**
1258
+ * One chat by id, else the visitor's newest; `conversations` lists them all. `aiLabel`: the messenger says "AI agent"
1259
+ * beside an answer's name, so the answer comes without its AI notice line (from 0.22.0).
1260
+ */
1261
+ chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation, aiLabel: "1" })}`, void 0, signal),
1262
+ /**
1263
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
1264
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
1265
+ */
1266
+ sendChat: (m) => post("/messages", { ...m, aiLabel: true }),
1267
+ /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
1268
+ uploadChatFile: (file, signal) => {
1269
+ const form = new FormData();
1270
+ form.append("key", c.apiKey);
1271
+ form.append("distinctId", c.distinctId);
1272
+ if (c.userHash) form.append("userHash", c.userHash);
1273
+ if ("uri" in file) form.append("file", file);
1274
+ else form.append("file", file, file.name || "file");
1275
+ return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
1276
+ },
1277
+ /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
1278
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
1279
+ articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
1280
+ article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
1281
+ messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
1282
+ rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
1283
+ };
1284
+ };
1285
+
1286
+ // src/batch.ts
1287
+ var MAX_BATCH = 100;
1288
+ var MAX_BYTES = 6e4;
1289
+ var byteLength = (s2) => {
1290
+ let n = 0;
1291
+ for (let i = 0; i < s2.length; i++) {
1292
+ const c = s2.charCodeAt(i);
1293
+ if (c < 128) n += 1;
1294
+ else if (c < 2048) n += 2;
1295
+ else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
1296
+ n += 4;
1297
+ i++;
1298
+ } else n += 3;
1299
+ }
1300
+ return n;
1301
+ };
1302
+ var batchRequests = (key, events, sdk, userHash) => {
1303
+ const head = `{"key":${JSON.stringify(key)},${userHash ? `"userHash":${JSON.stringify(userHash)},` : ""}"sdk":${JSON.stringify(sdk)},"events":[`;
1304
+ const empty = byteLength(head) + 2;
1305
+ const out = [];
1306
+ let chunk = [];
1307
+ let items = [];
1308
+ let size = empty;
1309
+ const close = () => {
1310
+ if (chunk.length) out.push({ body: `${head}${chunk.join(",")}]}`, events: items });
1311
+ chunk = [];
1312
+ items = [];
1313
+ size = empty;
1314
+ };
1315
+ for (const event of events) {
1316
+ const json = JSON.stringify(event);
1317
+ const n = byteLength(json) + 1;
1318
+ if (empty + n > MAX_BYTES) continue;
1319
+ if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();
1320
+ chunk.push(json);
1321
+ items.push(event);
1322
+ size += n;
1323
+ }
1324
+ close();
1325
+ return out;
1326
+ };
1327
+
1328
+ // src/errors.ts
1329
+ var MAX_STACK = 8 * 1024;
1330
+ var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1331
+ var describeError = (error) => {
1332
+ 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 };
1333
+ if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
1334
+ try {
1335
+ return { type: "Error", message: clip(JSON.stringify(error) ?? String(error), 1e3) };
1336
+ } catch {
1337
+ return { type: "Error", message: String(error) };
1338
+ }
1339
+ };
1340
+
1306
1341
  // src/paywall.ts
1307
1342
  var PAYWALL_EVENT = "$paywall";
1308
1343
  var paywallPages = (config, w, distinctId) => {
@@ -1833,7 +1868,7 @@ var createClient = (options, deps) => {
1833
1868
  }
1834
1869
  };
1835
1870
  const sendQueue = async () => {
1836
- if (!queue.length || consent === "off" || !apiKey) return;
1871
+ if (!queue.length || !apiKey || consent === "off" && queue.some((e) => e.event !== CONSENT_OFF_EVENT)) return;
1837
1872
  if (now() < backoffUntil) return;
1838
1873
  const snapshot = queue.slice();
1839
1874
  const requests = batchRequests(apiKey, snapshot, sdk, hashOf(sendId()));
@@ -2257,7 +2292,8 @@ var createClient = (options, deps) => {
2257
2292
  consent = next;
2258
2293
  if (next === "off") {
2259
2294
  recorder?.drop();
2260
- queue = [];
2295
+ queue = identified && before !== "off" ? [{ event: CONSENT_OFF_EVENT, distinctId: identified, ts: Date.now() }] : [];
2296
+ if (queue.length) void flush();
2261
2297
  session = null;
2262
2298
  errorQueue.length = 0;
2263
2299
  void Promise.all(Object.values(KEYS).map(remove));
@@ -3550,7 +3586,7 @@ var POLL_QUIET_MS = 6e4;
3550
3586
  var WAITING_FOR_MS = 10 * 6e4;
3551
3587
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
3552
3588
  var whoLabel = (from) => from === "visitor" ? t("You") : from === "ai" ? t("AI agent") : t("Team");
3553
- var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel(from);
3589
+ var who = (from, agentName) => from === "ai" && agentName?.trim() ? `${agentName.trim()} \xB7 ${t("AI agent")}` : whoLabel(from);
3554
3590
  var msgView = "home";
3555
3591
  var msgChat = null;
3556
3592
  var messengerOpen = opener();
@@ -3668,7 +3704,7 @@ var useChat = (client, o, open, view) => {
3668
3704
  if (fresh.length && o.haptics !== false) buzz();
3669
3705
  const shown2 = shownChat(at.current, all);
3670
3706
  const counted = fresh.filter((m) => !watch.current || m.chat !== shown2);
3671
- setUnseen((u) => [...u.filter((m) => all.some((c) => c.id === m.chat)), ...counted]);
3707
+ setUnseen((u) => [...u.filter(stillThere(t2, all)), ...counted]);
3672
3708
  list.current = all;
3673
3709
  setChats(all);
3674
3710
  setHasEmail(t2.hasEmail);
@@ -4544,6 +4580,7 @@ var useOnboarding = (id) => {
4544
4580
  const client = useClient();
4545
4581
  return useOnboardingItem(client.flows, id, client.startOnboarding);
4546
4582
  };
4583
+ var useOnboardingReport = (name, options) => useReportWith(useClient().track, name, options);
4547
4584
  var useFlag = (key) => {
4548
4585
  const client = useClient();
4549
4586
  useVersion(client);