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.mjs CHANGED
@@ -49,7 +49,14 @@ var SessionManager = class {
49
49
  }
50
50
  create() {
51
51
  const ts = this.now();
52
- return { sessionId: uuid(), startedAt: ts, lastActivity: ts, userId: null };
52
+ return {
53
+ sessionId: uuid(),
54
+ startedAt: ts,
55
+ lastActivity: ts,
56
+ userId: null,
57
+ tags: /* @__PURE__ */ new Set(),
58
+ metadata: {}
59
+ };
53
60
  }
54
61
  current() {
55
62
  return this.state;
@@ -57,6 +64,19 @@ var SessionManager = class {
57
64
  touch() {
58
65
  this.state.lastActivity = this.now();
59
66
  }
67
+ addTag(tag2) {
68
+ if (this.state.tags.has(tag2)) {
69
+ return false;
70
+ }
71
+ this.state.tags.add(tag2);
72
+ return true;
73
+ }
74
+ hasTag(tag2) {
75
+ return this.state.tags.has(tag2);
76
+ }
77
+ setMetadata(data) {
78
+ this.state.metadata = { ...this.state.metadata, ...data };
79
+ }
60
80
  setUser(userId) {
61
81
  const prev = this.state.userId;
62
82
  if (prev !== null && (userId === null || prev !== userId)) {
@@ -147,6 +167,8 @@ var Transport = class {
147
167
  return h;
148
168
  }
149
169
  };
170
+ var SNAPSHOT_AFTER_INTERACTION_MS = 400;
171
+ var UNCHANGED_RESEND_MS = 3e4;
150
172
  function mapAppState(state2) {
151
173
  if (state2 === "active") return "active";
152
174
  if (state2 === "background") return "background";
@@ -159,16 +181,25 @@ function createRecorder(deps) {
159
181
  const cleanups = [];
160
182
  let started = false;
161
183
  let wireframeTimer = null;
162
- function takeWireframeSnapshot() {
184
+ let interactionTimer = null;
185
+ let lastNodesKey = null;
186
+ let lastSentAt = 0;
187
+ function takeWireframeSnapshot(force = false) {
163
188
  if (!deps.captureViewHierarchy) return;
164
189
  try {
165
190
  const raw = deps.captureViewHierarchy();
166
191
  if (!raw || typeof raw !== "string") return;
167
192
  const payload = JSON.parse(raw);
168
193
  if (payload && Array.isArray(payload.nodes)) {
194
+ if (force && payload.nodes.length === 0) return;
195
+ const key = JSON.stringify(payload.nodes);
196
+ const now = deps.now();
197
+ if (!force && key === lastNodesKey && now - lastSentAt < UNCHANGED_RESEND_MS) return;
198
+ lastNodesKey = key;
199
+ lastSentAt = now;
169
200
  deps.onEvent({
170
201
  type: "wireframe",
171
- ts: deps.now(),
202
+ ts: now,
172
203
  version: typeof payload.version === "number" ? payload.version : 1,
173
204
  nodes: payload.nodes
174
205
  });
@@ -176,6 +207,20 @@ function createRecorder(deps) {
176
207
  } catch {
177
208
  }
178
209
  }
210
+ function scheduleWireframeSnapshot() {
211
+ if (!deps.captureWireframe || !deps.captureViewHierarchy) return;
212
+ if (interactionTimer) clearTimeout(interactionTimer);
213
+ interactionTimer = setTimeout(() => {
214
+ interactionTimer = null;
215
+ takeWireframeSnapshot();
216
+ }, SNAPSHOT_AFTER_INTERACTION_MS);
217
+ }
218
+ function cancelScheduledSnapshot() {
219
+ if (interactionTimer) {
220
+ clearTimeout(interactionTimer);
221
+ interactionTimer = null;
222
+ }
223
+ }
179
224
  function startWireframeTimer() {
180
225
  if (!deps.captureWireframe || !deps.captureViewHierarchy) return;
181
226
  if (wireframeTimer) return;
@@ -193,7 +238,11 @@ function createRecorder(deps) {
193
238
  }
194
239
  }
195
240
  return {
196
- takeWireframeSnapshot,
241
+ takeWireframeSnapshot: () => takeWireframeSnapshot(),
242
+ scheduleWireframeSnapshot,
243
+ resetWireframeBaseline() {
244
+ lastNodesKey = null;
245
+ },
197
246
  start() {
198
247
  if (started) return;
199
248
  started = true;
@@ -217,7 +266,9 @@ function createRecorder(deps) {
217
266
  if (mapped === "active") {
218
267
  startWireframeTimer();
219
268
  } else {
269
+ if (wireframeTimer) takeWireframeSnapshot(true);
220
270
  stopWireframeTimer();
271
+ cancelScheduledSnapshot();
221
272
  }
222
273
  }
223
274
  });
@@ -275,6 +326,7 @@ function createRecorder(deps) {
275
326
  if (!started) return;
276
327
  started = false;
277
328
  stopWireframeTimer();
329
+ cancelScheduledSnapshot();
278
330
  for (const fn of cleanups.splice(0)) {
279
331
  try {
280
332
  fn();
@@ -879,7 +931,7 @@ function captureViewHierarchy() {
879
931
  }
880
932
 
881
933
  // package.json
882
- var version = "0.2.0";
934
+ var version = "0.2.2";
883
935
  var CLINTPLAY_MASK_ID = "clintplay-mask";
884
936
  var ClintplayMaskView = React5.forwardRef(function ClintplayMaskView2({ children, testID, ...rest }, ref) {
885
937
  return /* @__PURE__ */ jsx(
@@ -1191,6 +1243,124 @@ function ClintplayNavigationTracker({
1191
1243
  // src/index.ts
1192
1244
  var SDK_VERSION = version;
1193
1245
  var state = null;
1246
+ var METADATA_MAX_KEYS = 50;
1247
+ var METADATA_MAX_DEPTH = 3;
1248
+ var METADATA_MAX_STRING_LENGTH = 1024;
1249
+ var METADATA_MAX_BYTES = 8192;
1250
+ var TAG_MAX_LENGTH = 128;
1251
+ var pendingMetadata = {};
1252
+ var pendingTags = /* @__PURE__ */ new Set();
1253
+ function sanitizeTag(rawTag) {
1254
+ if (typeof rawTag !== "string") return null;
1255
+ const trimmed = rawTag.trim();
1256
+ if (trimmed.length === 0) return null;
1257
+ const normalized = typeof trimmed.normalize === "function" ? trimmed.normalize("NFC") : trimmed;
1258
+ return normalized.slice(0, TAG_MAX_LENGTH);
1259
+ }
1260
+ function isPlainObject(val) {
1261
+ return typeof val === "object" && val !== null && !Array.isArray(val);
1262
+ }
1263
+ function sanitizeMetadataValue(val, depth, seen) {
1264
+ if (val === null || val === void 0) {
1265
+ return val;
1266
+ }
1267
+ if (typeof val === "function" || typeof val === "symbol") {
1268
+ return void 0;
1269
+ }
1270
+ if (typeof val === "string") {
1271
+ return val.slice(0, METADATA_MAX_STRING_LENGTH);
1272
+ }
1273
+ if (typeof val === "number") {
1274
+ return Number.isFinite(val) ? val : null;
1275
+ }
1276
+ if (typeof val === "boolean") {
1277
+ return val;
1278
+ }
1279
+ if (typeof val === "bigint") {
1280
+ return val.toString();
1281
+ }
1282
+ if (val instanceof Date) {
1283
+ return Number.isNaN(val.getTime()) ? null : val.toISOString();
1284
+ }
1285
+ if (typeof val === "object") {
1286
+ if (seen.has(val)) {
1287
+ return "[Circular]";
1288
+ }
1289
+ seen.add(val);
1290
+ try {
1291
+ if (depth > METADATA_MAX_DEPTH) {
1292
+ return void 0;
1293
+ }
1294
+ if (Array.isArray(val)) {
1295
+ const arr = [];
1296
+ for (const item of val) {
1297
+ const cleaned = sanitizeMetadataValue(item, depth + 1, seen);
1298
+ if (cleaned !== void 0) {
1299
+ arr.push(cleaned);
1300
+ }
1301
+ }
1302
+ return arr;
1303
+ }
1304
+ const obj = {};
1305
+ const entries = Object.entries(val);
1306
+ let keyCount = 0;
1307
+ for (const [k, v] of entries) {
1308
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
1309
+ continue;
1310
+ }
1311
+ if (keyCount >= METADATA_MAX_KEYS) {
1312
+ break;
1313
+ }
1314
+ const cleaned = sanitizeMetadataValue(v, depth + 1, seen);
1315
+ if (cleaned !== void 0) {
1316
+ obj[k] = cleaned;
1317
+ keyCount++;
1318
+ }
1319
+ }
1320
+ return obj;
1321
+ } finally {
1322
+ seen.delete(val);
1323
+ }
1324
+ }
1325
+ return void 0;
1326
+ }
1327
+ function sanitizeMetadata(data) {
1328
+ if (!isPlainObject(data)) {
1329
+ return null;
1330
+ }
1331
+ const seen = /* @__PURE__ */ new Set();
1332
+ seen.add(data);
1333
+ const result = {};
1334
+ const entries = Object.entries(data);
1335
+ let keyCount = 0;
1336
+ for (const [k, v] of entries) {
1337
+ if (k === "__proto__" || k === "constructor" || k === "prototype") {
1338
+ continue;
1339
+ }
1340
+ if (keyCount >= METADATA_MAX_KEYS) {
1341
+ break;
1342
+ }
1343
+ const cleaned = sanitizeMetadataValue(v, 1, seen);
1344
+ if (cleaned !== void 0) {
1345
+ result[k] = cleaned;
1346
+ keyCount++;
1347
+ }
1348
+ }
1349
+ try {
1350
+ let json = JSON.stringify(result);
1351
+ if (json.length > METADATA_MAX_BYTES) {
1352
+ const keys = Object.keys(result);
1353
+ while (keys.length > 0 && json.length > METADATA_MAX_BYTES) {
1354
+ const lastKey = keys.pop();
1355
+ delete result[lastKey];
1356
+ json = JSON.stringify(result);
1357
+ }
1358
+ }
1359
+ } catch {
1360
+ return null;
1361
+ }
1362
+ return result;
1363
+ }
1194
1364
  function deviceInfo() {
1195
1365
  const os = Platform.OS;
1196
1366
  const platform = os === "ios" ? "ios" : os === "android" ? "android" : "web";
@@ -1279,10 +1449,18 @@ function init(rawConfig) {
1279
1449
  buffer,
1280
1450
  session,
1281
1451
  transport,
1282
- recorder: { start: () => {
1283
- }, stop: () => {
1284
- }, takeWireframeSnapshot: () => {
1285
- } },
1452
+ recorder: {
1453
+ start: () => {
1454
+ },
1455
+ stop: () => {
1456
+ },
1457
+ takeWireframeSnapshot: () => {
1458
+ },
1459
+ scheduleWireframeSnapshot: () => {
1460
+ },
1461
+ resetWireframeBaseline: () => {
1462
+ }
1463
+ },
1286
1464
  networkInterceptor: null,
1287
1465
  consoleInterceptor: null,
1288
1466
  offlineQueue: rawConfig.asyncStorage ? new OfflineQueue({
@@ -1327,17 +1505,40 @@ function init(rawConfig) {
1327
1505
  captureConsoleLog: rawConfig.captureConsoleLog
1328
1506
  });
1329
1507
  }
1330
- buffer.push({
1331
- type: "meta",
1332
- ts: Date.now(),
1333
- sessionId: session.current().sessionId,
1334
- sdkVersion: SDK_VERSION,
1335
- device: deviceInfo()
1336
- });
1508
+ pushSessionMeta(local);
1509
+ if (Object.keys(pendingMetadata).length > 0) {
1510
+ local.session.setMetadata(pendingMetadata);
1511
+ if (!excluded.has("metadata")) {
1512
+ local.buffer.push({
1513
+ type: "metadata",
1514
+ ts: Date.now(),
1515
+ data: pendingMetadata
1516
+ });
1517
+ }
1518
+ pendingMetadata = {};
1519
+ }
1520
+ if (pendingTags.size > 0) {
1521
+ for (const t of pendingTags) {
1522
+ local.session.addTag(t);
1523
+ if (!excluded.has("tag")) {
1524
+ local.buffer.push({
1525
+ type: "tag",
1526
+ ts: Date.now(),
1527
+ data: { tag: t }
1528
+ });
1529
+ }
1530
+ }
1531
+ pendingTags.clear();
1532
+ }
1337
1533
  recorder.start();
1338
1534
  local.networkInterceptor?.start();
1339
1535
  local.consoleInterceptor?.start();
1340
- _setSink(onEvent);
1536
+ _setSink((event) => {
1537
+ onEvent(event);
1538
+ if (event.type === "navigation" || event.type === "touch" && event.kind === "end") {
1539
+ recorder.scheduleWireframeSnapshot();
1540
+ }
1541
+ });
1341
1542
  if (local.offlineQueue) {
1342
1543
  const q = local.offlineQueue;
1343
1544
  void q.hydrate().then(() => q.drain()).catch(
@@ -1348,13 +1549,7 @@ function init(rawConfig) {
1348
1549
  if (local.session.isStale()) {
1349
1550
  await flushOnce(local);
1350
1551
  if (local.session.rotateIfStale()) {
1351
- local.buffer.push({
1352
- type: "meta",
1353
- ts: Date.now(),
1354
- sessionId: local.session.current().sessionId,
1355
- sdkVersion: SDK_VERSION,
1356
- device: deviceInfo()
1357
- });
1552
+ pushSessionMeta(local);
1358
1553
  }
1359
1554
  }
1360
1555
  void flushOnce(local);
@@ -1373,13 +1568,7 @@ function identify(userId, traits) {
1373
1568
  }
1374
1569
  const rotated = state.session.setUser(normalizedUserId);
1375
1570
  if (rotated) {
1376
- state.buffer.push({
1377
- type: "meta",
1378
- ts: Date.now(),
1379
- sessionId: state.session.current().sessionId,
1380
- sdkVersion: SDK_VERSION,
1381
- device: deviceInfo()
1382
- });
1571
+ pushSessionMeta(state);
1383
1572
  }
1384
1573
  state.buffer.push({
1385
1574
  type: "identify",
@@ -1397,13 +1586,17 @@ function reset(options) {
1397
1586
  if (options?.clearOfflineQueue && state.offlineQueue) {
1398
1587
  void state.offlineQueue.clear();
1399
1588
  }
1400
- state.buffer.push({
1589
+ pushSessionMeta(state);
1590
+ }
1591
+ function pushSessionMeta(s) {
1592
+ s.buffer.push({
1401
1593
  type: "meta",
1402
1594
  ts: Date.now(),
1403
- sessionId: state.session.current().sessionId,
1595
+ sessionId: s.session.current().sessionId,
1404
1596
  sdkVersion: SDK_VERSION,
1405
1597
  device: deviceInfo()
1406
1598
  });
1599
+ s.recorder.resetWireframeBaseline();
1407
1600
  }
1408
1601
  function trackScreen(path, from) {
1409
1602
  if (!state?.active) return;
@@ -1414,9 +1607,7 @@ function trackScreen(path, from) {
1414
1607
  ...from ? { from } : {}
1415
1608
  });
1416
1609
  state.session.touch();
1417
- if (state.config.captureWireframe) {
1418
- state.recorder.takeWireframeSnapshot();
1419
- }
1610
+ state.recorder.scheduleWireframeSnapshot();
1420
1611
  }
1421
1612
  function reportError(err) {
1422
1613
  if (!state?.active) return;
@@ -1430,11 +1621,54 @@ function reportError(err) {
1430
1621
  fatal: false
1431
1622
  });
1432
1623
  }
1624
+ function setMetadata(data) {
1625
+ const sanitized = sanitizeMetadata(data);
1626
+ if (!sanitized || Object.keys(sanitized).length === 0) return;
1627
+ if (!state?.active) {
1628
+ pendingMetadata = { ...pendingMetadata, ...sanitized };
1629
+ return;
1630
+ }
1631
+ state.session.setMetadata(sanitized);
1632
+ state.session.touch();
1633
+ if (!state.excluded.has("metadata")) {
1634
+ state.buffer.push({
1635
+ type: "metadata",
1636
+ ts: Date.now(),
1637
+ data: sanitized
1638
+ });
1639
+ if (state.buffer.size() >= state.config.flushSize) {
1640
+ void flushOnce(state);
1641
+ }
1642
+ }
1643
+ }
1644
+ function tag(tagName) {
1645
+ const cleaned = sanitizeTag(tagName);
1646
+ if (!cleaned) return;
1647
+ if (!state?.active) {
1648
+ pendingTags.add(cleaned);
1649
+ return;
1650
+ }
1651
+ if (state.session.addTag(cleaned)) {
1652
+ state.session.touch();
1653
+ if (!state.excluded.has("tag")) {
1654
+ state.buffer.push({
1655
+ type: "tag",
1656
+ ts: Date.now(),
1657
+ data: { tag: cleaned }
1658
+ });
1659
+ if (state.buffer.size() >= state.config.flushSize) {
1660
+ void flushOnce(state);
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1433
1665
  async function flush() {
1434
1666
  if (!state?.active) return;
1435
1667
  await flushOnce(state);
1436
1668
  }
1437
1669
  async function stop() {
1670
+ pendingMetadata = {};
1671
+ pendingTags.clear();
1438
1672
  if (!state?.active) return;
1439
1673
  const s = state;
1440
1674
  s.active = false;
@@ -1464,12 +1698,14 @@ var Clintplay = {
1464
1698
  reset,
1465
1699
  trackScreen,
1466
1700
  reportError,
1701
+ setMetadata,
1702
+ tag,
1467
1703
  flush,
1468
1704
  stop,
1469
1705
  current,
1470
1706
  _getState
1471
1707
  };
1472
1708
 
1473
- export { CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, ClintplayMaskImage, ClintplayMaskTextInput, ClintplayMaskView, ClintplayNavigationTracker, SDK_VERSION, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeRoute, useClintplayNavigationTracking };
1709
+ export { CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, ClintplayMaskImage, ClintplayMaskTextInput, ClintplayMaskView, ClintplayNavigationTracker, METADATA_MAX_BYTES, METADATA_MAX_DEPTH, METADATA_MAX_KEYS, METADATA_MAX_STRING_LENGTH, SDK_VERSION, TAG_MAX_LENGTH, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeMetadata, sanitizeRoute, sanitizeTag, setMetadata, tag, useClintplayNavigationTracking };
1474
1710
  //# sourceMappingURL=index.mjs.map
1475
1711
  //# sourceMappingURL=index.mjs.map