clintplay-react-native 0.2.1 → 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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Session capture SDK for React Native / Expo apps — companion to [`clintplay-sdk`](../sdk) (web). Captures lifecycle, screen dimensions, navigation, JS errors, network requests (Fetch & XHR/Axios), console output, touches (screen-relative & target-relative), and native visual wireframe replay on iOS and Android.
4
4
 
5
- > **Status:** `0.2.1` — Production-ready JS runtime and Native TurboModule. Supports native view hierarchy traversal on iOS (`ios/ClintplayRN.mm`) and Android (`android/.../ClintplayRNModule.kt`), visual wireframe session replay, network interception for `fetch` and `XMLHttpRequest`/Axios, automatic navigation tracking, and intelligent auto-masking for sensitive fields.
5
+ > **Status:** `0.2.2` — Production-ready JS runtime and Native TurboModule. Supports native view hierarchy traversal on iOS (`ios/ClintplayRN.mm`) and Android (`android/.../ClintplayRNModule.kt`), visual wireframe session replay, network interception for `fetch` and `XMLHttpRequest`/Axios, automatic navigation tracking, and intelligent auto-masking for sensitive fields.
6
6
 
7
7
  ## Install
8
8
 
@@ -12,7 +12,7 @@ pnpm add clintplay-react-native
12
12
  npm install clintplay-react-native
13
13
  ```
14
14
 
15
- Peer deps: `react >=18`, `react-native >=0.74`. Optional peer: `@react-native-async-storage/async-storage` (enables the offline retry queue when present). Tested against Expo SDK 54 / RN 0.81 with New Architecture enabled.
15
+ Peer deps: `react >=18`, `react-native >=0.74`. Optional peer: `@react-native-async-storage/async-storage` (enables the offline retry queue when present). Android verified on Expo SDK 57 / RN 0.86.3 with New Architecture (debug and release builds, and wireframe capture on an emulator); iOS not re-verified on that version.
16
16
 
17
17
  In your `app.json`, register the config plugin:
18
18
 
@@ -1,19 +1,16 @@
1
- buildscript {
2
- // Match the consumer app's Kotlin version so we don't drag in a
3
- // conflicting toolchain. Most modern Expo SDK 54 / RN 0.81 apps
4
- // ship Kotlin 1.9.x.
5
- ext.kotlin_version = rootProject.ext.kotlin_version ?: '1.9.24'
6
-
7
- repositories {
8
- google()
9
- mavenCentral()
10
- }
11
- dependencies {
12
- classpath "com.android.tools.build:gradle:8.6.0"
13
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
- }
15
- }
16
-
1
+ // No `buildscript` block: this module is only built inside a host app, whose
2
+ // root build.gradle already puts the Android and Kotlin Gradle plugins on the
3
+ // classpath (bare RN and Expo templates alike). Don't read a Kotlin version from
4
+ // `rootProject.ext` either: Expo's template defines none, and in Groovy reading
5
+ // a missing `ext` property throws instead of returning null.
6
+
7
+ // On the classpath through the host's root build.gradle (resolved by the
8
+ // includeBuild in its settings.gradle). Runs the codegen for the
9
+ // `codegenConfig` in package.json (`ClintplayRNSpec`): autolinking adds that
10
+ // spec's `codegen/jni` folder to the app's CMake build and its C++
11
+ // ModuleProvider is what binds this TurboModule to JS, so without the plugin
12
+ // the app's CMake configure fails on a directory that was never generated.
13
+ apply plugin: 'com.facebook.react'
17
14
  apply plugin: 'com.android.library'
18
15
  apply plugin: 'org.jetbrains.kotlin.android'
19
16
 
@@ -43,9 +43,11 @@ import kotlin.math.round
43
43
  // coordinate conversion, background colors, extracted text, and
44
44
  // privacy auto-masking (clintplay-mask tags, password fields, and sensitive autofill hints).
45
45
  //
46
- // React Native's codegen emits `NativeClintplayRNSpec` from
47
- // `src/spec/NativeClintplayRN.ts` at build time. The TurboReactPackage
48
- // in `ClintplayRNPackage.kt` routes calls here.
46
+ // React Native's codegen emits `NativeClintplayRNSpec` and its C++ JSI glue
47
+ // from `src/spec/NativeClintplayRN.ts` at build time. This class does not
48
+ // extend that spec: the glue calls the methods below by name and JNI signature
49
+ // (`()Ljava/lang/String;`), so they must match the spec by hand. The
50
+ // TurboReactPackage in `ClintplayRNPackage.kt` routes calls here.
49
51
  @ReactModule(name = ClintplayRNModule.NAME)
50
52
  class ClintplayRNModule(reactContext: ReactApplicationContext) :
51
53
  ReactContextBaseJavaModule(reactContext), TurboModule {
@@ -326,7 +328,9 @@ class ClintplayRNModule(reactContext: ReactApplicationContext) :
326
328
 
327
329
  @ReactMethod(isBlockingSynchronousMethod = true)
328
330
  fun captureViewHierarchy(): String {
329
- val activity = currentActivity ?: return FALLBACK_JSON
331
+ // Not the inherited `currentActivity`: since RN 0.80 that is a deprecated
332
+ // protected Kotlin function, which property syntax does not resolve.
333
+ val activity = reactApplicationContext.currentActivity ?: return FALLBACK_JSON
330
334
 
331
335
  if (activity.isFinishing || activity.isDestroyed) {
332
336
  return FALLBACK_JSON
@@ -376,7 +380,7 @@ class ClintplayRNModule(reactContext: ReactApplicationContext) :
376
380
 
377
381
  companion object {
378
382
  const val NAME = "ClintplayRN"
379
- private const val VERSION = "0.2.1"
383
+ private const val VERSION = "0.2.2"
380
384
  private const val FALLBACK_JSON = "{\"version\":1,\"nodes\":[]}"
381
385
 
382
386
  private val NO_METHOD_SENTINEL = Any()
package/dist/index.d.mts CHANGED
@@ -7,6 +7,8 @@ interface SessionState {
7
7
  startedAt: number;
8
8
  lastActivity: number;
9
9
  userId: string | null;
10
+ tags: Set<string>;
11
+ metadata: Record<string, unknown>;
10
12
  }
11
13
  declare class SessionManager {
12
14
  private state;
@@ -15,13 +17,16 @@ declare class SessionManager {
15
17
  private create;
16
18
  current(): SessionState;
17
19
  touch(): void;
20
+ addTag(tag: string): boolean;
21
+ hasTag(tag: string): boolean;
22
+ setMetadata(data: Record<string, unknown>): void;
18
23
  setUser(userId: string | null): boolean;
19
24
  isStale(): boolean;
20
25
  rotateIfStale(): boolean;
21
26
  reset(): void;
22
27
  }
23
28
 
24
- type EventType = 'meta' | 'screen' | 'lifecycle' | 'navigation' | 'touch' | 'error' | 'log' | 'identify' | 'network' | 'wireframe';
29
+ type EventType = 'meta' | 'screen' | 'lifecycle' | 'navigation' | 'touch' | 'error' | 'log' | 'identify' | 'network' | 'wireframe' | 'metadata' | 'tag';
25
30
  interface DeviceInfo {
26
31
  platform: 'ios' | 'android' | 'web';
27
32
  osVersion?: string;
@@ -92,6 +97,18 @@ interface NetworkEvent {
92
97
  durationMs?: number;
93
98
  error?: string;
94
99
  }
100
+ interface MetadataEvent {
101
+ type: 'metadata';
102
+ ts: number;
103
+ data: Record<string, unknown>;
104
+ }
105
+ interface TagEvent {
106
+ type: 'tag';
107
+ ts: number;
108
+ data: {
109
+ tag: string;
110
+ };
111
+ }
95
112
  interface WireframeNode {
96
113
  id: number;
97
114
  type: string;
@@ -117,7 +134,7 @@ interface WireframeEvent {
117
134
  version: number;
118
135
  nodes: WireframeNode[];
119
136
  }
120
- type ReplayEvent = MetaEvent | ScreenEvent | LifecycleEvent | NavigationEvent | TouchEvent | ErrorEvent | LogEvent | IdentifyEvent | NetworkEvent | WireframeEvent;
137
+ type ReplayEvent = MetaEvent | ScreenEvent | LifecycleEvent | NavigationEvent | TouchEvent | ErrorEvent | LogEvent | IdentifyEvent | NetworkEvent | WireframeEvent | MetadataEvent | TagEvent;
121
138
  interface BackendEvent {
122
139
  type: string;
123
140
  timestamp: number;
@@ -390,6 +407,13 @@ interface InternalConfig {
390
407
  debug: boolean;
391
408
  captureWireframe: boolean;
392
409
  }
410
+ declare const METADATA_MAX_KEYS = 50;
411
+ declare const METADATA_MAX_DEPTH = 3;
412
+ declare const METADATA_MAX_STRING_LENGTH = 1024;
413
+ declare const METADATA_MAX_BYTES = 8192;
414
+ declare const TAG_MAX_LENGTH = 128;
415
+ declare function sanitizeTag(rawTag: unknown): string | null;
416
+ declare function sanitizeMetadata(data: unknown): Record<string, unknown> | null;
393
417
  declare function formatEventForBatch(event: ReplayEvent | BackendEvent, defaultSessionId: string): BackendEvent;
394
418
  declare function init(rawConfig: ClintplayReactNativeConfig): typeof Clintplay;
395
419
  declare function identify(userId: string | number | null, traits?: Record<string, unknown>): void;
@@ -398,6 +422,8 @@ declare function reset(options?: {
398
422
  }): void;
399
423
  declare function trackScreen(path: string, from?: string): void;
400
424
  declare function reportError(err: unknown): void;
425
+ declare function setMetadata(data: Record<string, unknown>): void;
426
+ declare function tag(tagName: string): void;
401
427
  declare function flush(): Promise<void>;
402
428
  declare function stop(): Promise<void>;
403
429
  declare function current(): {
@@ -423,10 +449,12 @@ declare const Clintplay: {
423
449
  reset: typeof reset;
424
450
  trackScreen: typeof trackScreen;
425
451
  reportError: typeof reportError;
452
+ setMetadata: typeof setMetadata;
453
+ tag: typeof tag;
426
454
  flush: typeof flush;
427
455
  stop: typeof stop;
428
456
  current: typeof current;
429
457
  _getState: typeof _getState;
430
458
  };
431
459
 
432
- export { type BackendEvent, CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, type ClintplayCaptureProviderProps, ClintplayMaskImage, type ClintplayMaskImageProps, ClintplayMaskTextInput, type ClintplayMaskTextInputProps, ClintplayMaskView, type ClintplayMaskViewProps, ClintplayNavigationTracker, type ClintplayNavigationTrackerProps, type ClintplayReactNativeConfig, type DeviceInfo, type NavigationRouteLike, type NavigationStateLike, type NavigationStateListenerCallback, type NavigationStateListenerOptions, type ReplayBatch, type ReplayEvent, SDK_VERSION, type UseClintplayNavigationOptions, type UseClintplayNavigationResult, type WireframeEvent, type WireframeNode, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeRoute, useClintplayNavigationTracking };
460
+ export { type BackendEvent, CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, type ClintplayCaptureProviderProps, ClintplayMaskImage, type ClintplayMaskImageProps, ClintplayMaskTextInput, type ClintplayMaskTextInputProps, ClintplayMaskView, type ClintplayMaskViewProps, ClintplayNavigationTracker, type ClintplayNavigationTrackerProps, type ClintplayReactNativeConfig, type DeviceInfo, METADATA_MAX_BYTES, METADATA_MAX_DEPTH, METADATA_MAX_KEYS, METADATA_MAX_STRING_LENGTH, type MetadataEvent, type NavigationRouteLike, type NavigationStateLike, type NavigationStateListenerCallback, type NavigationStateListenerOptions, type ReplayBatch, type ReplayEvent, SDK_VERSION, TAG_MAX_LENGTH, type TagEvent, type UseClintplayNavigationOptions, type UseClintplayNavigationResult, type WireframeEvent, type WireframeNode, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeMetadata, sanitizeRoute, sanitizeTag, setMetadata, tag, useClintplayNavigationTracking };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ interface SessionState {
7
7
  startedAt: number;
8
8
  lastActivity: number;
9
9
  userId: string | null;
10
+ tags: Set<string>;
11
+ metadata: Record<string, unknown>;
10
12
  }
11
13
  declare class SessionManager {
12
14
  private state;
@@ -15,13 +17,16 @@ declare class SessionManager {
15
17
  private create;
16
18
  current(): SessionState;
17
19
  touch(): void;
20
+ addTag(tag: string): boolean;
21
+ hasTag(tag: string): boolean;
22
+ setMetadata(data: Record<string, unknown>): void;
18
23
  setUser(userId: string | null): boolean;
19
24
  isStale(): boolean;
20
25
  rotateIfStale(): boolean;
21
26
  reset(): void;
22
27
  }
23
28
 
24
- type EventType = 'meta' | 'screen' | 'lifecycle' | 'navigation' | 'touch' | 'error' | 'log' | 'identify' | 'network' | 'wireframe';
29
+ type EventType = 'meta' | 'screen' | 'lifecycle' | 'navigation' | 'touch' | 'error' | 'log' | 'identify' | 'network' | 'wireframe' | 'metadata' | 'tag';
25
30
  interface DeviceInfo {
26
31
  platform: 'ios' | 'android' | 'web';
27
32
  osVersion?: string;
@@ -92,6 +97,18 @@ interface NetworkEvent {
92
97
  durationMs?: number;
93
98
  error?: string;
94
99
  }
100
+ interface MetadataEvent {
101
+ type: 'metadata';
102
+ ts: number;
103
+ data: Record<string, unknown>;
104
+ }
105
+ interface TagEvent {
106
+ type: 'tag';
107
+ ts: number;
108
+ data: {
109
+ tag: string;
110
+ };
111
+ }
95
112
  interface WireframeNode {
96
113
  id: number;
97
114
  type: string;
@@ -117,7 +134,7 @@ interface WireframeEvent {
117
134
  version: number;
118
135
  nodes: WireframeNode[];
119
136
  }
120
- type ReplayEvent = MetaEvent | ScreenEvent | LifecycleEvent | NavigationEvent | TouchEvent | ErrorEvent | LogEvent | IdentifyEvent | NetworkEvent | WireframeEvent;
137
+ type ReplayEvent = MetaEvent | ScreenEvent | LifecycleEvent | NavigationEvent | TouchEvent | ErrorEvent | LogEvent | IdentifyEvent | NetworkEvent | WireframeEvent | MetadataEvent | TagEvent;
121
138
  interface BackendEvent {
122
139
  type: string;
123
140
  timestamp: number;
@@ -390,6 +407,13 @@ interface InternalConfig {
390
407
  debug: boolean;
391
408
  captureWireframe: boolean;
392
409
  }
410
+ declare const METADATA_MAX_KEYS = 50;
411
+ declare const METADATA_MAX_DEPTH = 3;
412
+ declare const METADATA_MAX_STRING_LENGTH = 1024;
413
+ declare const METADATA_MAX_BYTES = 8192;
414
+ declare const TAG_MAX_LENGTH = 128;
415
+ declare function sanitizeTag(rawTag: unknown): string | null;
416
+ declare function sanitizeMetadata(data: unknown): Record<string, unknown> | null;
393
417
  declare function formatEventForBatch(event: ReplayEvent | BackendEvent, defaultSessionId: string): BackendEvent;
394
418
  declare function init(rawConfig: ClintplayReactNativeConfig): typeof Clintplay;
395
419
  declare function identify(userId: string | number | null, traits?: Record<string, unknown>): void;
@@ -398,6 +422,8 @@ declare function reset(options?: {
398
422
  }): void;
399
423
  declare function trackScreen(path: string, from?: string): void;
400
424
  declare function reportError(err: unknown): void;
425
+ declare function setMetadata(data: Record<string, unknown>): void;
426
+ declare function tag(tagName: string): void;
401
427
  declare function flush(): Promise<void>;
402
428
  declare function stop(): Promise<void>;
403
429
  declare function current(): {
@@ -423,10 +449,12 @@ declare const Clintplay: {
423
449
  reset: typeof reset;
424
450
  trackScreen: typeof trackScreen;
425
451
  reportError: typeof reportError;
452
+ setMetadata: typeof setMetadata;
453
+ tag: typeof tag;
426
454
  flush: typeof flush;
427
455
  stop: typeof stop;
428
456
  current: typeof current;
429
457
  _getState: typeof _getState;
430
458
  };
431
459
 
432
- export { type BackendEvent, CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, type ClintplayCaptureProviderProps, ClintplayMaskImage, type ClintplayMaskImageProps, ClintplayMaskTextInput, type ClintplayMaskTextInputProps, ClintplayMaskView, type ClintplayMaskViewProps, ClintplayNavigationTracker, type ClintplayNavigationTrackerProps, type ClintplayReactNativeConfig, type DeviceInfo, type NavigationRouteLike, type NavigationStateLike, type NavigationStateListenerCallback, type NavigationStateListenerOptions, type ReplayBatch, type ReplayEvent, SDK_VERSION, type UseClintplayNavigationOptions, type UseClintplayNavigationResult, type WireframeEvent, type WireframeNode, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeRoute, useClintplayNavigationTracking };
460
+ export { type BackendEvent, CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, type ClintplayCaptureProviderProps, ClintplayMaskImage, type ClintplayMaskImageProps, ClintplayMaskTextInput, type ClintplayMaskTextInputProps, ClintplayMaskView, type ClintplayMaskViewProps, ClintplayNavigationTracker, type ClintplayNavigationTrackerProps, type ClintplayReactNativeConfig, type DeviceInfo, METADATA_MAX_BYTES, METADATA_MAX_DEPTH, METADATA_MAX_KEYS, METADATA_MAX_STRING_LENGTH, type MetadataEvent, type NavigationRouteLike, type NavigationStateLike, type NavigationStateListenerCallback, type NavigationStateListenerOptions, type ReplayBatch, type ReplayEvent, SDK_VERSION, TAG_MAX_LENGTH, type TagEvent, type UseClintplayNavigationOptions, type UseClintplayNavigationResult, type WireframeEvent, type WireframeNode, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, init, sanitizeMetadata, sanitizeRoute, sanitizeTag, setMetadata, tag, useClintplayNavigationTracking };
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)) {
@@ -917,7 +937,7 @@ function captureViewHierarchy() {
917
937
  }
918
938
 
919
939
  // package.json
920
- var version = "0.2.1";
940
+ var version = "0.2.2";
921
941
  var CLINTPLAY_MASK_ID = "clintplay-mask";
922
942
  var ClintplayMaskView = React5__default.default.forwardRef(function ClintplayMaskView2({ children, testID, ...rest }, ref) {
923
943
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1229,6 +1249,124 @@ function ClintplayNavigationTracker({
1229
1249
  // src/index.ts
1230
1250
  var SDK_VERSION = version;
1231
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
+ }
1232
1370
  function deviceInfo() {
1233
1371
  const os = reactNative.Platform.OS;
1234
1372
  const platform = os === "ios" ? "ios" : os === "android" ? "android" : "web";
@@ -1374,6 +1512,30 @@ function init(rawConfig) {
1374
1512
  });
1375
1513
  }
1376
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
+ }
1377
1539
  recorder.start();
1378
1540
  local.networkInterceptor?.start();
1379
1541
  local.consoleInterceptor?.start();
@@ -1465,11 +1627,54 @@ function reportError(err) {
1465
1627
  fatal: false
1466
1628
  });
1467
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
+ }
1468
1671
  async function flush() {
1469
1672
  if (!state?.active) return;
1470
1673
  await flushOnce(state);
1471
1674
  }
1472
1675
  async function stop() {
1676
+ pendingMetadata = {};
1677
+ pendingTags.clear();
1473
1678
  if (!state?.active) return;
1474
1679
  const s = state;
1475
1680
  s.active = false;
@@ -1499,6 +1704,8 @@ var Clintplay = {
1499
1704
  reset,
1500
1705
  trackScreen,
1501
1706
  reportError,
1707
+ setMetadata,
1708
+ tag,
1502
1709
  flush,
1503
1710
  stop,
1504
1711
  current,
@@ -1512,13 +1719,22 @@ exports.ClintplayMaskImage = ClintplayMaskImage;
1512
1719
  exports.ClintplayMaskTextInput = ClintplayMaskTextInput;
1513
1720
  exports.ClintplayMaskView = ClintplayMaskView;
1514
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;
1515
1726
  exports.SDK_VERSION = SDK_VERSION;
1727
+ exports.TAG_MAX_LENGTH = TAG_MAX_LENGTH;
1516
1728
  exports._resetLastTrackedRoute = _resetLastTrackedRoute;
1517
1729
  exports.createClintplayNavigationStateListener = createClintplayNavigationStateListener;
1518
1730
  exports.formatEventForBatch = formatEventForBatch;
1519
1731
  exports.getActiveRouteName = getActiveRouteName;
1520
1732
  exports.init = init;
1733
+ exports.sanitizeMetadata = sanitizeMetadata;
1521
1734
  exports.sanitizeRoute = sanitizeRoute;
1735
+ exports.sanitizeTag = sanitizeTag;
1736
+ exports.setMetadata = setMetadata;
1737
+ exports.tag = tag;
1522
1738
  exports.useClintplayNavigationTracking = useClintplayNavigationTracking;
1523
1739
  //# sourceMappingURL=index.js.map
1524
1740
  //# sourceMappingURL=index.js.map