clintplay-react-native 0.2.0 → 0.2.2

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/index.js CHANGED
@@ -55,7 +55,14 @@ var SessionManager = class {
55
55
  }
56
56
  create() {
57
57
  const ts = this.now();
58
- return { sessionId: uuid(), startedAt: ts, lastActivity: ts, userId: null };
58
+ return {
59
+ sessionId: uuid(),
60
+ startedAt: ts,
61
+ lastActivity: ts,
62
+ userId: null,
63
+ tags: /* @__PURE__ */ new Set(),
64
+ metadata: {}
65
+ };
59
66
  }
60
67
  current() {
61
68
  return this.state;
@@ -63,6 +70,19 @@ var SessionManager = class {
63
70
  touch() {
64
71
  this.state.lastActivity = this.now();
65
72
  }
73
+ addTag(tag2) {
74
+ if (this.state.tags.has(tag2)) {
75
+ return false;
76
+ }
77
+ this.state.tags.add(tag2);
78
+ return true;
79
+ }
80
+ hasTag(tag2) {
81
+ return this.state.tags.has(tag2);
82
+ }
83
+ setMetadata(data) {
84
+ this.state.metadata = { ...this.state.metadata, ...data };
85
+ }
66
86
  setUser(userId) {
67
87
  const prev = this.state.userId;
68
88
  if (prev !== null && (userId === null || prev !== userId)) {
@@ -153,6 +173,8 @@ var Transport = class {
153
173
  return h;
154
174
  }
155
175
  };
176
+ var SNAPSHOT_AFTER_INTERACTION_MS = 400;
177
+ var UNCHANGED_RESEND_MS = 3e4;
156
178
  function mapAppState(state2) {
157
179
  if (state2 === "active") return "active";
158
180
  if (state2 === "background") return "background";
@@ -165,16 +187,25 @@ function createRecorder(deps) {
165
187
  const cleanups = [];
166
188
  let started = false;
167
189
  let wireframeTimer = null;
168
- function takeWireframeSnapshot() {
190
+ let interactionTimer = null;
191
+ let lastNodesKey = null;
192
+ let lastSentAt = 0;
193
+ function takeWireframeSnapshot(force = false) {
169
194
  if (!deps.captureViewHierarchy) return;
170
195
  try {
171
196
  const raw = deps.captureViewHierarchy();
172
197
  if (!raw || typeof raw !== "string") return;
173
198
  const payload = JSON.parse(raw);
174
199
  if (payload && Array.isArray(payload.nodes)) {
200
+ if (force && payload.nodes.length === 0) return;
201
+ const key = JSON.stringify(payload.nodes);
202
+ const now = deps.now();
203
+ if (!force && key === lastNodesKey && now - lastSentAt < UNCHANGED_RESEND_MS) return;
204
+ lastNodesKey = key;
205
+ lastSentAt = now;
175
206
  deps.onEvent({
176
207
  type: "wireframe",
177
- ts: deps.now(),
208
+ ts: now,
178
209
  version: typeof payload.version === "number" ? payload.version : 1,
179
210
  nodes: payload.nodes
180
211
  });
@@ -182,6 +213,20 @@ function createRecorder(deps) {
182
213
  } catch {
183
214
  }
184
215
  }
216
+ function scheduleWireframeSnapshot() {
217
+ if (!deps.captureWireframe || !deps.captureViewHierarchy) return;
218
+ if (interactionTimer) clearTimeout(interactionTimer);
219
+ interactionTimer = setTimeout(() => {
220
+ interactionTimer = null;
221
+ takeWireframeSnapshot();
222
+ }, SNAPSHOT_AFTER_INTERACTION_MS);
223
+ }
224
+ function cancelScheduledSnapshot() {
225
+ if (interactionTimer) {
226
+ clearTimeout(interactionTimer);
227
+ interactionTimer = null;
228
+ }
229
+ }
185
230
  function startWireframeTimer() {
186
231
  if (!deps.captureWireframe || !deps.captureViewHierarchy) return;
187
232
  if (wireframeTimer) return;
@@ -199,7 +244,11 @@ function createRecorder(deps) {
199
244
  }
200
245
  }
201
246
  return {
202
- takeWireframeSnapshot,
247
+ takeWireframeSnapshot: () => takeWireframeSnapshot(),
248
+ scheduleWireframeSnapshot,
249
+ resetWireframeBaseline() {
250
+ lastNodesKey = null;
251
+ },
203
252
  start() {
204
253
  if (started) return;
205
254
  started = true;
@@ -223,7 +272,9 @@ function createRecorder(deps) {
223
272
  if (mapped === "active") {
224
273
  startWireframeTimer();
225
274
  } else {
275
+ if (wireframeTimer) takeWireframeSnapshot(true);
226
276
  stopWireframeTimer();
277
+ cancelScheduledSnapshot();
227
278
  }
228
279
  }
229
280
  });
@@ -281,6 +332,7 @@ function createRecorder(deps) {
281
332
  if (!started) return;
282
333
  started = false;
283
334
  stopWireframeTimer();
335
+ cancelScheduledSnapshot();
284
336
  for (const fn of cleanups.splice(0)) {
285
337
  try {
286
338
  fn();
@@ -885,7 +937,7 @@ function captureViewHierarchy() {
885
937
  }
886
938
 
887
939
  // package.json
888
- var version = "0.2.0";
940
+ var version = "0.2.2";
889
941
  var CLINTPLAY_MASK_ID = "clintplay-mask";
890
942
  var ClintplayMaskView = React5__default.default.forwardRef(function ClintplayMaskView2({ children, testID, ...rest }, ref) {
891
943
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1197,6 +1249,124 @@ function ClintplayNavigationTracker({
1197
1249
  // src/index.ts
1198
1250
  var SDK_VERSION = version;
1199
1251
  var state = null;
1252
+ var METADATA_MAX_KEYS = 50;
1253
+ var METADATA_MAX_DEPTH = 3;
1254
+ var METADATA_MAX_STRING_LENGTH = 1024;
1255
+ var METADATA_MAX_BYTES = 8192;
1256
+ var TAG_MAX_LENGTH = 128;
1257
+ var pendingMetadata = {};
1258
+ var pendingTags = /* @__PURE__ */ new Set();
1259
+ function sanitizeTag(rawTag) {
1260
+ if (typeof rawTag !== "string") return null;
1261
+ const trimmed = rawTag.trim();
1262
+ if (trimmed.length === 0) return null;
1263
+ const normalized = typeof trimmed.normalize === "function" ? trimmed.normalize("NFC") : trimmed;
1264
+ return normalized.slice(0, TAG_MAX_LENGTH);
1265
+ }
1266
+ function isPlainObject(val) {
1267
+ return typeof val === "object" && val !== null && !Array.isArray(val);
1268
+ }
1269
+ function sanitizeMetadataValue(val, depth, seen) {
1270
+ if (val === null || val === void 0) {
1271
+ return val;
1272
+ }
1273
+ if (typeof val === "function" || typeof val === "symbol") {
1274
+ return void 0;
1275
+ }
1276
+ if (typeof val === "string") {
1277
+ return val.slice(0, METADATA_MAX_STRING_LENGTH);
1278
+ }
1279
+ if (typeof val === "number") {
1280
+ return Number.isFinite(val) ? val : null;
1281
+ }
1282
+ if (typeof val === "boolean") {
1283
+ return val;
1284
+ }
1285
+ if (typeof val === "bigint") {
1286
+ return val.toString();
1287
+ }
1288
+ if (val instanceof Date) {
1289
+ return Number.isNaN(val.getTime()) ? null : val.toISOString();
1290
+ }
1291
+ if (typeof val === "object") {
1292
+ if (seen.has(val)) {
1293
+ return "[Circular]";
1294
+ }
1295
+ seen.add(val);
1296
+ try {
1297
+ if (depth > METADATA_MAX_DEPTH) {
1298
+ return void 0;
1299
+ }
1300
+ if (Array.isArray(val)) {
1301
+ const arr = [];
1302
+ for (const item of val) {
1303
+ const cleaned = sanitizeMetadataValue(item, depth + 1, seen);
1304
+ if (cleaned !== void 0) {
1305
+ arr.push(cleaned);
1306
+ }
1307
+ }
1308
+ return arr;
1309
+ }
1310
+ const obj = {};
1311
+ const entries = Object.entries(val);
1312
+ let keyCount = 0;
1313
+ for (const [k, v] of entries) {
1314
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
1315
+ continue;
1316
+ }
1317
+ if (keyCount >= METADATA_MAX_KEYS) {
1318
+ break;
1319
+ }
1320
+ const cleaned = sanitizeMetadataValue(v, depth + 1, seen);
1321
+ if (cleaned !== void 0) {
1322
+ obj[k] = cleaned;
1323
+ keyCount++;
1324
+ }
1325
+ }
1326
+ return obj;
1327
+ } finally {
1328
+ seen.delete(val);
1329
+ }
1330
+ }
1331
+ return void 0;
1332
+ }
1333
+ function sanitizeMetadata(data) {
1334
+ if (!isPlainObject(data)) {
1335
+ return null;
1336
+ }
1337
+ const seen = /* @__PURE__ */ new Set();
1338
+ seen.add(data);
1339
+ const result = {};
1340
+ const entries = Object.entries(data);
1341
+ let keyCount = 0;
1342
+ for (const [k, v] of entries) {
1343
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
1344
+ continue;
1345
+ }
1346
+ if (keyCount >= METADATA_MAX_KEYS) {
1347
+ break;
1348
+ }
1349
+ const cleaned = sanitizeMetadataValue(v, 1, seen);
1350
+ if (cleaned !== void 0) {
1351
+ result[k] = cleaned;
1352
+ keyCount++;
1353
+ }
1354
+ }
1355
+ try {
1356
+ let json = JSON.stringify(result);
1357
+ if (json.length > METADATA_MAX_BYTES) {
1358
+ const keys = Object.keys(result);
1359
+ while (keys.length > 0 && json.length > METADATA_MAX_BYTES) {
1360
+ const lastKey = keys.pop();
1361
+ delete result[lastKey];
1362
+ json = JSON.stringify(result);
1363
+ }
1364
+ }
1365
+ } catch {
1366
+ return null;
1367
+ }
1368
+ return result;
1369
+ }
1200
1370
  function deviceInfo() {
1201
1371
  const os = reactNative.Platform.OS;
1202
1372
  const platform = os === "ios" ? "ios" : os === "android" ? "android" : "web";
@@ -1285,10 +1455,18 @@ function init(rawConfig) {
1285
1455
  buffer,
1286
1456
  session,
1287
1457
  transport,
1288
- recorder: { start: () => {
1289
- }, stop: () => {
1290
- }, takeWireframeSnapshot: () => {
1291
- } },
1458
+ recorder: {
1459
+ start: () => {
1460
+ },
1461
+ stop: () => {
1462
+ },
1463
+ takeWireframeSnapshot: () => {
1464
+ },
1465
+ scheduleWireframeSnapshot: () => {
1466
+ },
1467
+ resetWireframeBaseline: () => {
1468
+ }
1469
+ },
1292
1470
  networkInterceptor: null,
1293
1471
  consoleInterceptor: null,
1294
1472
  offlineQueue: rawConfig.asyncStorage ? new OfflineQueue({
@@ -1333,17 +1511,40 @@ function init(rawConfig) {
1333
1511
  captureConsoleLog: rawConfig.captureConsoleLog
1334
1512
  });
1335
1513
  }
1336
- buffer.push({
1337
- type: "meta",
1338
- ts: Date.now(),
1339
- sessionId: session.current().sessionId,
1340
- sdkVersion: SDK_VERSION,
1341
- device: deviceInfo()
1342
- });
1514
+ pushSessionMeta(local);
1515
+ if (Object.keys(pendingMetadata).length > 0) {
1516
+ local.session.setMetadata(pendingMetadata);
1517
+ if (!excluded.has("metadata")) {
1518
+ local.buffer.push({
1519
+ type: "metadata",
1520
+ ts: Date.now(),
1521
+ data: pendingMetadata
1522
+ });
1523
+ }
1524
+ pendingMetadata = {};
1525
+ }
1526
+ if (pendingTags.size > 0) {
1527
+ for (const t of pendingTags) {
1528
+ local.session.addTag(t);
1529
+ if (!excluded.has("tag")) {
1530
+ local.buffer.push({
1531
+ type: "tag",
1532
+ ts: Date.now(),
1533
+ data: { tag: t }
1534
+ });
1535
+ }
1536
+ }
1537
+ pendingTags.clear();
1538
+ }
1343
1539
  recorder.start();
1344
1540
  local.networkInterceptor?.start();
1345
1541
  local.consoleInterceptor?.start();
1346
- _setSink(onEvent);
1542
+ _setSink((event) => {
1543
+ onEvent(event);
1544
+ if (event.type === "navigation" || event.type === "touch" && event.kind === "end") {
1545
+ recorder.scheduleWireframeSnapshot();
1546
+ }
1547
+ });
1347
1548
  if (local.offlineQueue) {
1348
1549
  const q = local.offlineQueue;
1349
1550
  void q.hydrate().then(() => q.drain()).catch(
@@ -1354,13 +1555,7 @@ function init(rawConfig) {
1354
1555
  if (local.session.isStale()) {
1355
1556
  await flushOnce(local);
1356
1557
  if (local.session.rotateIfStale()) {
1357
- local.buffer.push({
1358
- type: "meta",
1359
- ts: Date.now(),
1360
- sessionId: local.session.current().sessionId,
1361
- sdkVersion: SDK_VERSION,
1362
- device: deviceInfo()
1363
- });
1558
+ pushSessionMeta(local);
1364
1559
  }
1365
1560
  }
1366
1561
  void flushOnce(local);
@@ -1379,13 +1574,7 @@ function identify(userId, traits) {
1379
1574
  }
1380
1575
  const rotated = state.session.setUser(normalizedUserId);
1381
1576
  if (rotated) {
1382
- state.buffer.push({
1383
- type: "meta",
1384
- ts: Date.now(),
1385
- sessionId: state.session.current().sessionId,
1386
- sdkVersion: SDK_VERSION,
1387
- device: deviceInfo()
1388
- });
1577
+ pushSessionMeta(state);
1389
1578
  }
1390
1579
  state.buffer.push({
1391
1580
  type: "identify",
@@ -1403,13 +1592,17 @@ function reset(options) {
1403
1592
  if (options?.clearOfflineQueue && state.offlineQueue) {
1404
1593
  void state.offlineQueue.clear();
1405
1594
  }
1406
- state.buffer.push({
1595
+ pushSessionMeta(state);
1596
+ }
1597
+ function pushSessionMeta(s) {
1598
+ s.buffer.push({
1407
1599
  type: "meta",
1408
1600
  ts: Date.now(),
1409
- sessionId: state.session.current().sessionId,
1601
+ sessionId: s.session.current().sessionId,
1410
1602
  sdkVersion: SDK_VERSION,
1411
1603
  device: deviceInfo()
1412
1604
  });
1605
+ s.recorder.resetWireframeBaseline();
1413
1606
  }
1414
1607
  function trackScreen(path, from) {
1415
1608
  if (!state?.active) return;
@@ -1420,9 +1613,7 @@ function trackScreen(path, from) {
1420
1613
  ...from ? { from } : {}
1421
1614
  });
1422
1615
  state.session.touch();
1423
- if (state.config.captureWireframe) {
1424
- state.recorder.takeWireframeSnapshot();
1425
- }
1616
+ state.recorder.scheduleWireframeSnapshot();
1426
1617
  }
1427
1618
  function reportError(err) {
1428
1619
  if (!state?.active) return;
@@ -1436,11 +1627,54 @@ function reportError(err) {
1436
1627
  fatal: false
1437
1628
  });
1438
1629
  }
1630
+ function setMetadata(data) {
1631
+ const sanitized = sanitizeMetadata(data);
1632
+ if (!sanitized || Object.keys(sanitized).length === 0) return;
1633
+ if (!state?.active) {
1634
+ pendingMetadata = { ...pendingMetadata, ...sanitized };
1635
+ return;
1636
+ }
1637
+ state.session.setMetadata(sanitized);
1638
+ state.session.touch();
1639
+ if (!state.excluded.has("metadata")) {
1640
+ state.buffer.push({
1641
+ type: "metadata",
1642
+ ts: Date.now(),
1643
+ data: sanitized
1644
+ });
1645
+ if (state.buffer.size() >= state.config.flushSize) {
1646
+ void flushOnce(state);
1647
+ }
1648
+ }
1649
+ }
1650
+ function tag(tagName) {
1651
+ const cleaned = sanitizeTag(tagName);
1652
+ if (!cleaned) return;
1653
+ if (!state?.active) {
1654
+ pendingTags.add(cleaned);
1655
+ return;
1656
+ }
1657
+ if (state.session.addTag(cleaned)) {
1658
+ state.session.touch();
1659
+ if (!state.excluded.has("tag")) {
1660
+ state.buffer.push({
1661
+ type: "tag",
1662
+ ts: Date.now(),
1663
+ data: { tag: cleaned }
1664
+ });
1665
+ if (state.buffer.size() >= state.config.flushSize) {
1666
+ void flushOnce(state);
1667
+ }
1668
+ }
1669
+ }
1670
+ }
1439
1671
  async function flush() {
1440
1672
  if (!state?.active) return;
1441
1673
  await flushOnce(state);
1442
1674
  }
1443
1675
  async function stop() {
1676
+ pendingMetadata = {};
1677
+ pendingTags.clear();
1444
1678
  if (!state?.active) return;
1445
1679
  const s = state;
1446
1680
  s.active = false;
@@ -1470,6 +1704,8 @@ var Clintplay = {
1470
1704
  reset,
1471
1705
  trackScreen,
1472
1706
  reportError,
1707
+ setMetadata,
1708
+ tag,
1473
1709
  flush,
1474
1710
  stop,
1475
1711
  current,
@@ -1483,13 +1719,22 @@ exports.ClintplayMaskImage = ClintplayMaskImage;
1483
1719
  exports.ClintplayMaskTextInput = ClintplayMaskTextInput;
1484
1720
  exports.ClintplayMaskView = ClintplayMaskView;
1485
1721
  exports.ClintplayNavigationTracker = ClintplayNavigationTracker;
1722
+ exports.METADATA_MAX_BYTES = METADATA_MAX_BYTES;
1723
+ exports.METADATA_MAX_DEPTH = METADATA_MAX_DEPTH;
1724
+ exports.METADATA_MAX_KEYS = METADATA_MAX_KEYS;
1725
+ exports.METADATA_MAX_STRING_LENGTH = METADATA_MAX_STRING_LENGTH;
1486
1726
  exports.SDK_VERSION = SDK_VERSION;
1727
+ exports.TAG_MAX_LENGTH = TAG_MAX_LENGTH;
1487
1728
  exports._resetLastTrackedRoute = _resetLastTrackedRoute;
1488
1729
  exports.createClintplayNavigationStateListener = createClintplayNavigationStateListener;
1489
1730
  exports.formatEventForBatch = formatEventForBatch;
1490
1731
  exports.getActiveRouteName = getActiveRouteName;
1491
1732
  exports.init = init;
1733
+ exports.sanitizeMetadata = sanitizeMetadata;
1492
1734
  exports.sanitizeRoute = sanitizeRoute;
1735
+ exports.sanitizeTag = sanitizeTag;
1736
+ exports.setMetadata = setMetadata;
1737
+ exports.tag = tag;
1493
1738
  exports.useClintplayNavigationTracking = useClintplayNavigationTracking;
1494
1739
  //# sourceMappingURL=index.js.map
1495
1740
  //# sourceMappingURL=index.js.map