@scalebun/react-native 1.10.7 → 1.11.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.
Files changed (35) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
  2. package/dist/scalebun.full.js +236 -187
  3. package/dist/scalebun.slim.js +235 -186
  4. package/ios/Capture/InteractionTracker.swift +8 -4
  5. package/lib/commonjs/analytics/EventTracker.js +5 -5
  6. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  7. package/lib/commonjs/core/constants/version.js +7 -2
  8. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
  9. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  10. package/lib/commonjs/features/journey/uiState.js +8 -1
  11. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  12. package/lib/commonjs/features/session/SessionManager.js +37 -38
  13. package/lib/module/analytics/EventTracker.js +5 -5
  14. package/lib/module/analytics/automaticEvents.js +3 -2
  15. package/lib/module/core/constants/version.js +7 -2
  16. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
  17. package/lib/module/features/journey/interactionProtocol.js +38 -0
  18. package/lib/module/features/journey/uiState.js +8 -1
  19. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  20. package/lib/module/features/session/SessionManager.js +37 -38
  21. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  22. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  23. package/lib/typescript/core/constants/version.d.ts +7 -2
  24. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  25. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  26. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  27. package/package.json +3 -2
  28. package/src/analytics/EventTracker.ts +5 -5
  29. package/src/analytics/automaticEvents.ts +4 -0
  30. package/src/core/constants/version.ts +7 -2
  31. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
  32. package/src/features/journey/interactionProtocol.ts +65 -0
  33. package/src/features/journey/uiState.ts +9 -4
  34. package/src/features/session/JourneyEventPipeline.ts +7 -5
  35. package/src/features/session/SessionManager.ts +75 -38
@@ -600,7 +600,7 @@ var SDK_VERSION;
600
600
  var init_version = __esm({
601
601
  "lib/module/core/constants/version.js"() {
602
602
  "use strict";
603
- SDK_VERSION = "1.10.7";
603
+ SDK_VERSION = "1.11.0";
604
604
  }
605
605
  });
606
606
 
@@ -2459,22 +2459,23 @@ var init_JourneyEventPipeline = __esm({
2459
2459
  emit(type, opts) {
2460
2460
  try {
2461
2461
  const key = `${type}:${opts?.subtype ?? ""}`;
2462
- const now2 = Date.now();
2463
- if (key === this.lastEventKey && now2 - this.lastEventTs < this.config.dedupeWindowMs) {
2462
+ const receivedAt = Date.now();
2463
+ const occurredAt = opts?.timestamp ?? receivedAt;
2464
+ if (key === this.lastEventKey && receivedAt - this.lastEventTs < this.config.dedupeWindowMs) {
2464
2465
  const incomingConfidence = opts?.payload?.confidence;
2465
2466
  if (incomingConfidence === "high" && this.lastEventConfidence !== "high") {
2466
- this._replaceLastEvent(key, now2, opts);
2467
+ this._replaceLastEvent(key, receivedAt, opts);
2467
2468
  }
2468
2469
  return null;
2469
2470
  }
2470
2471
  this.lastEventKey = key;
2471
- this.lastEventTs = now2;
2472
+ this.lastEventTs = receivedAt;
2472
2473
  this.lastEventConfidence = opts?.payload?.confidence ?? null;
2473
2474
  const event = {
2474
2475
  eventId: generateEventId2(),
2475
2476
  sessionId: this.sessionId,
2476
2477
  journeyId: opts?.journeyId,
2477
- ts: now2,
2478
+ ts: occurredAt,
2478
2479
  type,
2479
2480
  subtype: opts?.subtype,
2480
2481
  severity: opts?.severity ?? inferSeverity(type),
@@ -3574,6 +3575,99 @@ var init_calibrationContext = __esm({
3574
3575
  }
3575
3576
  });
3576
3577
 
3578
+ // lib/module/analytics/automaticEvents.js
3579
+ function compactProperties(properties) {
3580
+ const out = {
3581
+ capture_source: "automatic"
3582
+ };
3583
+ for (const [key, value] of Object.entries(properties ?? {})) {
3584
+ if (value !== void 0) out[key] = value;
3585
+ }
3586
+ return out;
3587
+ }
3588
+ function emitAutomaticEvent(name, properties, timestamp) {
3589
+ const event = {
3590
+ name,
3591
+ properties: compactProperties(properties),
3592
+ timestamp
3593
+ };
3594
+ if (listeners.size === 0) {
3595
+ pending2.push(event);
3596
+ if (pending2.length > MAX_PENDING) pending2.shift();
3597
+ return;
3598
+ }
3599
+ for (const listener of listeners) {
3600
+ try {
3601
+ listener(event);
3602
+ } catch {
3603
+ }
3604
+ }
3605
+ }
3606
+ function subscribeAutomaticEvents(listener) {
3607
+ listeners.add(listener);
3608
+ if (pending2.length > 0) {
3609
+ const buffered = pending2.splice(0, pending2.length);
3610
+ for (const event of buffered) {
3611
+ try {
3612
+ listener(event);
3613
+ } catch {
3614
+ }
3615
+ }
3616
+ }
3617
+ return () => {
3618
+ listeners.delete(listener);
3619
+ };
3620
+ }
3621
+ var listeners, pending2, MAX_PENDING;
3622
+ var init_automaticEvents = __esm({
3623
+ "lib/module/analytics/automaticEvents.js"() {
3624
+ "use strict";
3625
+ listeners = /* @__PURE__ */ new Set();
3626
+ pending2 = [];
3627
+ MAX_PENDING = 50;
3628
+ }
3629
+ });
3630
+
3631
+ // lib/module/features/journey/interactionProtocol.js
3632
+ function generateInteractionId() {
3633
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3634
+ }
3635
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
3636
+ let best;
3637
+ let bestDelta = toleranceMs + 1;
3638
+ for (const start of starts) {
3639
+ const delta = Math.abs(start.occurredAt - occurredAt);
3640
+ if (delta < bestDelta) {
3641
+ best = start;
3642
+ bestDelta = delta;
3643
+ }
3644
+ }
3645
+ return bestDelta <= toleranceMs ? best : void 0;
3646
+ }
3647
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3648
+ return {
3649
+ gesture_type: payload.gestureType,
3650
+ screen_name: screenName,
3651
+ interaction_id: payload.interaction_id,
3652
+ interaction_protocol: payload.interaction_protocol,
3653
+ state_status: payload.state_status,
3654
+ ui: payload.ui,
3655
+ target_id: payload.target_id,
3656
+ normalized_x: payload.normalizedX,
3657
+ normalized_y: payload.normalizedY,
3658
+ direction: payload.direction,
3659
+ duration_ms: payload.durationMs,
3660
+ canonical_mirror: canonicalMirror || void 0
3661
+ };
3662
+ }
3663
+ var INTERACTION_PROTOCOL_VERSION;
3664
+ var init_interactionProtocol = __esm({
3665
+ "lib/module/features/journey/interactionProtocol.js"() {
3666
+ "use strict";
3667
+ INTERACTION_PROTOCOL_VERSION = 1;
3668
+ }
3669
+ });
3670
+
3577
3671
  // lib/module/features/session/SessionManager.js
3578
3672
  var SessionManager_exports = {};
3579
3673
  __export(SessionManager_exports, {
@@ -3582,7 +3676,7 @@ __export(SessionManager_exports, {
3582
3676
  function generateSessionId2() {
3583
3677
  return `ses-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
3584
3678
  }
3585
- var HEATMAP_MAX_INTERACTIONS_PER_WINDOW, _instance2, SessionManager;
3679
+ var _instance2, SessionManager;
3586
3680
  var init_SessionManager = __esm({
3587
3681
  "lib/module/features/session/SessionManager.js"() {
3588
3682
  "use strict";
@@ -3599,7 +3693,8 @@ var init_SessionManager = __esm({
3599
3693
  init_redaction();
3600
3694
  init_calibrationContext();
3601
3695
  init_device();
3602
- HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
3696
+ init_automaticEvents();
3697
+ init_interactionProtocol();
3603
3698
  _instance2 = null;
3604
3699
  SessionManager = class _SessionManager {
3605
3700
  desktopTransport = null;
@@ -3610,15 +3705,6 @@ var init_SessionManager = __esm({
3610
3705
  * lane even when no replay recording is active. Additive, opt-in (default off).
3611
3706
  */
3612
3707
  _captureInteractionHeatmap = false;
3613
- /**
3614
- * Sampling cap for the analytics-lane heatmap emission. Now that capture is
3615
- * ON by default, an unbounded one-event-per-gesture stream could materially
3616
- * inflate ingest volume. We cap emitted interactions per analytics-session
3617
- * window (finalize-scoped per foreground): the first N gestures define the
3618
- * hotspot shape; the long tail is dropped. Resets when the window changes.
3619
- */
3620
- _heatmapWindowSessionId = null;
3621
- _heatmapWindowCount = 0;
3622
3708
  session = null;
3623
3709
  active = false;
3624
3710
  timeoutTimer = null;
@@ -4099,18 +4185,11 @@ var init_SessionManager = __esm({
4099
4185
  * injected sensitive keys (e.g. a label) are stripped before transport.
4100
4186
  * - Never throws.
4101
4187
  */
4102
- _emitInteractionToAnalytics(gestureType, payload) {
4103
- if (!this._captureInteractionHeatmap) return;
4104
- if (this.active) return;
4188
+ _emitInteractionToAnalytics(gestureType, payload, occurredAt, screenName) {
4189
+ if (!this._captureInteractionHeatmap) return false;
4190
+ if (this.active) return false;
4105
4191
  const adapter = this._backendTransport;
4106
- if (!adapter) return;
4107
- const windowId = adapter.analyticsSessionId ?? "";
4108
- if (windowId !== this._heatmapWindowSessionId) {
4109
- this._heatmapWindowSessionId = windowId;
4110
- this._heatmapWindowCount = 0;
4111
- }
4112
- if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
4113
- this._heatmapWindowCount++;
4192
+ if (!adapter) return false;
4114
4193
  try {
4115
4194
  this._normalizeInteractionPayload(payload);
4116
4195
  const safePayload = redactBody(payload);
@@ -4118,16 +4197,18 @@ var init_SessionManager = __esm({
4118
4197
  eventId: generateEventId(),
4119
4198
  // sessionId is (re)stamped by the analytics lane at flush time.
4120
4199
  sessionId: adapter.analyticsSessionId ?? "",
4121
- ts: Date.now(),
4200
+ ts: occurredAt,
4122
4201
  type: "USER_ACTION",
4123
4202
  subtype: `gesture:${gestureType}`,
4124
- screen: this._lastKnownScreen ?? void 0,
4203
+ screen: screenName ?? this._lastKnownScreen ?? void 0,
4125
4204
  payload: safePayload,
4126
4205
  source: "user"
4127
4206
  };
4128
4207
  adapter.trackEvent(event);
4208
+ return true;
4129
4209
  } catch (err) {
4130
4210
  logger.error("[SessionManager] heatmap analytics emit failed:", err);
4211
+ return false;
4131
4212
  }
4132
4213
  }
4133
4214
  /**
@@ -4293,13 +4374,14 @@ var init_SessionManager = __esm({
4293
4374
  * Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
4294
4375
  * Also triggers frame capture for desktop-initiated recordings.
4295
4376
  */
4296
- onUserAction(subtype, payload) {
4377
+ onUserAction(subtype, payload, context) {
4297
4378
  if (!this.active) return;
4298
4379
  this.emitEvent("USER_ACTION", {
4299
4380
  subtype,
4300
- screen: this._lastKnownScreen ?? void 0,
4381
+ screen: context?.screen ?? this._lastKnownScreen ?? void 0,
4301
4382
  payload,
4302
- source: "user"
4383
+ source: "user",
4384
+ timestamp: context?.timestamp
4303
4385
  });
4304
4386
  }
4305
4387
  /**
@@ -4307,7 +4389,8 @@ var init_SessionManager = __esm({
4307
4389
  * Emits a USER_ACTION event with gesture-specific subtype and payload.
4308
4390
  */
4309
4391
  onGestureDetected(gestureType, details) {
4310
- const dedupSig = `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4392
+ const suppliedInteractionId = details?.interactionId;
4393
+ const dedupSig = suppliedInteractionId ? `id:${suppliedInteractionId}` : `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
4311
4394
  const nowTs = Date.now();
4312
4395
  if (dedupSig === this._lastGestureSig && nowTs - this._lastGestureTs <= _SessionManager.GESTURE_DEDUP_WINDOW_MS) {
4313
4396
  this._lastGestureTs = nowTs;
@@ -4315,7 +4398,15 @@ var init_SessionManager = __esm({
4315
4398
  }
4316
4399
  this._lastGestureSig = dedupSig;
4317
4400
  this._lastGestureTs = nowTs;
4401
+ const interactionId = suppliedInteractionId ?? generateInteractionId();
4402
+ const occurredAt = details?.occurredAt ?? Date.now();
4403
+ const stateStatus = details?.stateStatus ?? (details?.ui ? "captured_nonempty" : "not_captured");
4318
4404
  const payload = {
4405
+ interaction_id: interactionId,
4406
+ interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
4407
+ state_status: stateStatus,
4408
+ ui: details?.ui,
4409
+ target_id: details?.targetId,
4319
4410
  gestureType,
4320
4411
  x: details?.x,
4321
4412
  y: details?.y,
@@ -4353,12 +4444,23 @@ var init_SessionManager = __esm({
4353
4444
  screenHeight: details?.screenHeight,
4354
4445
  platform: details?.platform
4355
4446
  };
4356
- this._emitInteractionToAnalytics(gestureType, {
4447
+ const analyticsReplayCarrier = this._emitInteractionToAnalytics(gestureType, {
4357
4448
  ...payload
4358
- });
4449
+ }, occurredAt, details?.screenName);
4450
+ const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
4451
+ if (details?.emitAutomaticAnalytics) {
4452
+ try {
4453
+ const safePayload = redactBody(payload);
4454
+ emitAutomaticEvent("element_interacted", automaticInteractionProperties(safePayload, details?.screenName ?? this._lastKnownScreen ?? void 0, replayCarrier), occurredAt);
4455
+ } catch {
4456
+ }
4457
+ }
4359
4458
  if (!this.active) return;
4360
4459
  const target = typeof details?.target === "string" ? details.target.trim() : "";
4361
- this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload);
4460
+ this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload, {
4461
+ screen: details?.screenName,
4462
+ timestamp: occurredAt
4463
+ });
4362
4464
  }
4363
4465
  /**
4364
4466
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -6229,58 +6331,6 @@ var init_batching = __esm({
6229
6331
  }
6230
6332
  });
6231
6333
 
6232
- // lib/module/analytics/automaticEvents.js
6233
- function compactProperties(properties) {
6234
- const out = {
6235
- capture_source: "automatic"
6236
- };
6237
- for (const [key, value] of Object.entries(properties ?? {})) {
6238
- if (value !== void 0) out[key] = value;
6239
- }
6240
- return out;
6241
- }
6242
- function emitAutomaticEvent(name, properties) {
6243
- const event = {
6244
- name,
6245
- properties: compactProperties(properties)
6246
- };
6247
- if (listeners.size === 0) {
6248
- pending3.push(event);
6249
- if (pending3.length > MAX_PENDING2) pending3.shift();
6250
- return;
6251
- }
6252
- for (const listener of listeners) {
6253
- try {
6254
- listener(event);
6255
- } catch {
6256
- }
6257
- }
6258
- }
6259
- function subscribeAutomaticEvents(listener) {
6260
- listeners.add(listener);
6261
- if (pending3.length > 0) {
6262
- const buffered = pending3.splice(0, pending3.length);
6263
- for (const event of buffered) {
6264
- try {
6265
- listener(event);
6266
- } catch {
6267
- }
6268
- }
6269
- }
6270
- return () => {
6271
- listeners.delete(listener);
6272
- };
6273
- }
6274
- var listeners, pending3, MAX_PENDING2;
6275
- var init_automaticEvents = __esm({
6276
- "lib/module/analytics/automaticEvents.js"() {
6277
- "use strict";
6278
- listeners = /* @__PURE__ */ new Set();
6279
- pending3 = [];
6280
- MAX_PENDING2 = 50;
6281
- }
6282
- });
6283
-
6284
6334
  // lib/module/analytics/EventTracker.js
6285
6335
  function uuid() {
6286
6336
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -6349,7 +6399,7 @@ var init_EventTracker = __esm({
6349
6399
  this.started = true;
6350
6400
  if (this.cfg.automaticEventTracking) {
6351
6401
  this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
6352
- this.track(event.name, event.properties);
6402
+ this.track(event.name, event.properties, event.timestamp);
6353
6403
  });
6354
6404
  }
6355
6405
  if (this.cfg.autoLifecycleEvents) {
@@ -6453,9 +6503,9 @@ var init_EventTracker = __esm({
6453
6503
  }
6454
6504
  }
6455
6505
  // ─── tracking ────────────────────────────────────────────────────────────
6456
- track(eventName, properties) {
6506
+ track(eventName, properties, timestamp) {
6457
6507
  try {
6458
- this.enqueue(this.buildEnvelope(eventName, properties));
6508
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
6459
6509
  try {
6460
6510
  this.cfg.onEvent?.(eventName);
6461
6511
  } catch {
@@ -6559,7 +6609,7 @@ var init_EventTracker = __esm({
6559
6609
  };
6560
6610
  }
6561
6611
  // ─── internals ─────────────────────────────────────────────────────────────
6562
- buildEnvelope(eventName, properties) {
6612
+ buildEnvelope(eventName, properties, timestamp) {
6563
6613
  const ctx = this.cfg.context ?? {};
6564
6614
  let canonicalSessionId;
6565
6615
  try {
@@ -6570,7 +6620,7 @@ var init_EventTracker = __esm({
6570
6620
  const env = {
6571
6621
  event_id: uuid(),
6572
6622
  event_name: eventName,
6573
- event_time: Date.now(),
6623
+ event_time: timestamp ?? Date.now(),
6574
6624
  app_id: this.cfg.appId,
6575
6625
  platform: this.cfg.platform ?? resolveEventPlatform(),
6576
6626
  installation_id: this.installationId,
@@ -8056,7 +8106,14 @@ function clearUiState(name) {
8056
8106
  }
8057
8107
  function uiStateSignature() {
8058
8108
  if (!declared.size) return void 0;
8059
- return [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`).join(";").slice(0, 96);
8109
+ const pairs = [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`);
8110
+ let signature = "";
8111
+ for (const pair of pairs) {
8112
+ const next = signature ? `${signature};${pair}` : pair;
8113
+ if (next.length > 96) break;
8114
+ signature = next;
8115
+ }
8116
+ return signature || void 0;
8060
8117
  }
8061
8118
  var declared, clean;
8062
8119
  var init_uiState = __esm({
@@ -11013,14 +11070,14 @@ function installRejectionHandler(onRejection, deps) {
11013
11070
  allRejections: true,
11014
11071
  onUnhandled: (id, error) => {
11015
11072
  const err = toError(error);
11016
- pending2.set(id, err);
11073
+ pending3.set(id, err);
11017
11074
  onRejection(err);
11018
11075
  if (isDev) {
11019
11076
  console.warn(`Possible unhandled promise rejection (id: ${id}):`, err?.message ?? err);
11020
11077
  }
11021
11078
  },
11022
11079
  onHandled: (id) => {
11023
- pending2.delete(id);
11080
+ pending3.delete(id);
11024
11081
  if (isDev) {
11025
11082
  console.warn(`Promise rejection handled late (id: ${id}) \u2014 it was already reported.`);
11026
11083
  }
@@ -11030,7 +11087,7 @@ function installRejectionHandler(onRejection, deps) {
11030
11087
  return true;
11031
11088
  }) ?? false;
11032
11089
  }
11033
- var pending2 = /* @__PURE__ */ new Map();
11090
+ var pending3 = /* @__PURE__ */ new Map();
11034
11091
 
11035
11092
  // lib/module/features/crash/CrashFeature.js
11036
11093
  init_CrashReporter();
@@ -11347,7 +11404,7 @@ var AppLaunchCollector = class {
11347
11404
  };
11348
11405
 
11349
11406
  // lib/module/features/performance/collectors/ScreenLoadCollector.js
11350
- var MAX_PENDING = 20;
11407
+ var MAX_PENDING2 = 20;
11351
11408
  var ScreenLoadCollector = class {
11352
11409
  pending = /* @__PURE__ */ new Map();
11353
11410
  lastScreen = null;
@@ -11362,7 +11419,7 @@ var ScreenLoadCollector = class {
11362
11419
  */
11363
11420
  markStart(screenName) {
11364
11421
  if (!this.config.screenLoad) return;
11365
- if (this.pending.size >= MAX_PENDING) {
11422
+ if (this.pending.size >= MAX_PENDING2) {
11366
11423
  const oldest = this.pending.keys().next().value;
11367
11424
  if (oldest) this.pending.delete(oldest);
11368
11425
  }
@@ -29983,7 +30040,6 @@ function useEngagePrompt() {
29983
30040
  // lib/module/features/journey/ScaleBunDebugRoot.js
29984
30041
  var import_react9 = __toESM(require("react"));
29985
30042
  var import_react_native42 = require("react-native");
29986
- init_automaticEvents();
29987
30043
 
29988
30044
  // lib/module/features/journey/touchTarget.js
29989
30045
  var MAX_DEPTH = 12;
@@ -30055,6 +30111,7 @@ function describeTouchTarget(t) {
30055
30111
 
30056
30112
  // lib/module/features/journey/ScaleBunDebugRoot.js
30057
30113
  init_device();
30114
+ init_interactionProtocol();
30058
30115
  var import_jsx_runtime8 = require("react/jsx-runtime");
30059
30116
  var _bootstrap = null;
30060
30117
  function getBootstrap() {
@@ -30244,7 +30301,9 @@ function ScaleBunDebugRoot({
30244
30301
  }, [navigationRef]);
30245
30302
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
30246
30303
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
30247
- const pendingJsEmitRef = (0, import_react9.useRef)(null);
30304
+ const touchStartRef = (0, import_react9.useRef)(null);
30305
+ const interactionStartsRef = (0, import_react9.useRef)([]);
30306
+ const pendingJsEmitsRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
30248
30307
  (0, import_react9.useEffect)(() => {
30249
30308
  let subscription = null;
30250
30309
  try {
@@ -30255,9 +30314,12 @@ function ScaleBunDebugRoot({
30255
30314
  try {
30256
30315
  lastNativeTouchTsRef.current = Date.now();
30257
30316
  nativeTrackingConfirmedRef.current = true;
30258
- if (pendingJsEmitRef.current !== null) {
30259
- clearTimeout(pendingJsEmitRef.current);
30260
- pendingJsEmitRef.current = null;
30317
+ const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
30318
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
30319
+ if (start) {
30320
+ const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
30321
+ if (pending4 !== void 0) clearTimeout(pending4);
30322
+ pendingJsEmitsRef.current.delete(start.interactionId);
30261
30323
  }
30262
30324
  const {
30263
30325
  SessionManager: SessionManager2
@@ -30276,6 +30338,15 @@ function ScaleBunDebugRoot({
30276
30338
  } catch {
30277
30339
  }
30278
30340
  sm.onGestureDetected(event.gestureType || "tap", {
30341
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
30342
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
30343
+ occurredAt: nativeOccurredAt,
30344
+ ui: start?.ui,
30345
+ stateStatus: start?.stateStatus ?? "not_captured",
30346
+ target: start?.target,
30347
+ targetId: start?.targetId,
30348
+ screenName: start?.screenName,
30349
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30279
30350
  x: event.rawX,
30280
30351
  y: event.rawY,
30281
30352
  // Use the actual native end coords when present, not the down
@@ -30308,9 +30379,10 @@ function ScaleBunDebugRoot({
30308
30379
  subscription?.remove();
30309
30380
  } catch {
30310
30381
  }
30382
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
30383
+ pendingJsEmitsRef.current.clear();
30311
30384
  };
30312
- }, []);
30313
- const touchStartRef = (0, import_react9.useRef)(null);
30385
+ }, [captureAutomaticInteractions]);
30314
30386
  const rootRectRef = (0, import_react9.useRef)(null);
30315
30387
  const measureRoot = () => {
30316
30388
  try {
@@ -30342,11 +30414,39 @@ function ScaleBunDebugRoot({
30342
30414
  const handleTouchStart = (e) => {
30343
30415
  try {
30344
30416
  const touch = e.nativeEvent;
30345
- touchStartRef.current = {
30417
+ const target = resolveTouchTarget(e);
30418
+ let ui;
30419
+ let screenName;
30420
+ try {
30421
+ const {
30422
+ uiStateSignature: uiStateSignature2
30423
+ } = (init_uiState(), __toCommonJS(uiState_exports));
30424
+ ui = uiStateSignature2();
30425
+ } catch {
30426
+ }
30427
+ try {
30428
+ const {
30429
+ AutoScreenDetector: AutoScreenDetector2
30430
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30431
+ screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30432
+ } catch {
30433
+ }
30434
+ const start = {
30435
+ interactionId: generateInteractionId(),
30436
+ occurredAt: Date.now(),
30346
30437
  x: touch.pageX,
30347
30438
  y: touch.pageY,
30348
- ts: Date.now()
30439
+ locationX: touch.locationX,
30440
+ locationY: touch.locationY,
30441
+ target: describeTouchTarget(target),
30442
+ targetId: target?.testID,
30443
+ screenName,
30444
+ ui,
30445
+ stateStatus: ui ? "captured_nonempty" : "not_instrumented",
30446
+ emitAutomaticAnalytics: captureAutomaticInteractions
30349
30447
  };
30448
+ touchStartRef.current = start;
30449
+ interactionStartsRef.current = interactionStartsRef.current.filter((candidate) => start.occurredAt - candidate.occurredAt < 5e3).concat(start).slice(-8);
30350
30450
  } catch {
30351
30451
  }
30352
30452
  };
@@ -30377,21 +30477,29 @@ function ScaleBunDebugRoot({
30377
30477
  }
30378
30478
  measureRoot();
30379
30479
  const rootRect = rootRectRef.current;
30380
- const tapped = describeTouchTarget(resolveTouchTarget(e));
30480
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
30381
30481
  const gestureDetails = {
30382
30482
  target: tapped,
30383
- x: touch.pageX,
30384
- y: touch.pageY,
30385
- pageX: touch.pageX,
30386
- pageY: touch.pageY,
30387
- locationX: touch.locationX,
30388
- locationY: touch.locationY,
30483
+ interactionId: start?.interactionId ?? generateInteractionId(),
30484
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
30485
+ occurredAt: start?.occurredAt ?? Date.now(),
30486
+ ui: start?.ui,
30487
+ stateStatus: start?.stateStatus ?? "not_captured",
30488
+ targetId: start?.targetId,
30489
+ screenName: start?.screenName,
30490
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30491
+ x: start?.x ?? touch.pageX,
30492
+ y: start?.y ?? touch.pageY,
30493
+ pageX: start?.x ?? touch.pageX,
30494
+ pageY: start?.y ?? touch.pageY,
30495
+ locationX: start?.locationX ?? touch.locationX,
30496
+ locationY: start?.locationY ?? touch.locationY,
30389
30497
  viewportWidth: vpW > 0 ? vpW : void 0,
30390
30498
  viewportHeight: vpH > 0 ? vpH : void 0
30391
30499
  };
30392
30500
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
30393
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
30394
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
30501
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
30502
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
30395
30503
  gestureDetails.normalizedX = nx;
30396
30504
  gestureDetails.normalizedY = ny;
30397
30505
  gestureDetails.normalizedPrecomputed = true;
@@ -30422,7 +30530,7 @@ function ScaleBunDebugRoot({
30422
30530
  const dx = touch.pageX - start.x;
30423
30531
  const dy = touch.pageY - start.y;
30424
30532
  const dist = Math.sqrt(dx * dx + dy * dy);
30425
- const duration = Date.now() - start.ts;
30533
+ const duration = Date.now() - start.occurredAt;
30426
30534
  gestureDetails.duration = duration;
30427
30535
  if (dist >= 15) {
30428
30536
  gestureDetails.endX = touch.pageX;
@@ -30440,60 +30548,16 @@ function ScaleBunDebugRoot({
30440
30548
  gestureType = "long_press";
30441
30549
  }
30442
30550
  }
30443
- const targetInfo = _extractTarget(e);
30444
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
30445
- if (captureAutomaticInteractions) {
30446
- let screen;
30447
- try {
30448
- const {
30449
- AutoScreenDetector: AutoScreenDetector2
30450
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30451
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30452
- } catch {
30453
- }
30454
- let ui;
30455
- try {
30456
- const {
30457
- uiStateSignature: uiStateSignature2
30458
- } = (init_uiState(), __toCommonJS(uiState_exports));
30459
- ui = uiStateSignature2();
30460
- } catch {
30461
- }
30462
- emitAutomaticEvent("element_interacted", {
30463
- gesture_type: gestureType,
30464
- screen_name: screen,
30465
- /* THIS MAP ENUMERATES. A field added to the payload and forgotten here reaches the
30466
- backend as undefined with no error anywhere — the recurring defect class in this
30467
- codebase. `ui` is the key the grid aggregate reads for state, byte-identical to
30468
- the web SDK's, so one dashboard control queries both platforms. */
30469
- ui,
30470
- // testID/nativeID is an author-controlled stable identifier.
30471
- // Accessibility labels and rendered text are deliberately omitted.
30472
- target_id: targetInfo?.testId,
30473
- normalized_x: gestureDetails.normalizedX,
30474
- normalized_y: gestureDetails.normalizedY,
30475
- direction: gestureDetails.direction,
30476
- duration_ms: gestureDetails.duration
30477
- });
30478
- }
30479
30551
  if (gestureType === "tap") {
30480
30552
  try {
30481
30553
  const {
30482
30554
  gestureTriggerDetector: gestureTriggerDetector2
30483
30555
  } = (init_gestureTriggerDetector(), __toCommonJS(gestureTriggerDetector_exports));
30484
- let screen;
30485
- try {
30486
- const {
30487
- AutoScreenDetector: AutoScreenDetector2
30488
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30489
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30490
- } catch {
30491
- }
30492
30556
  gestureTriggerDetector2.recordTap({
30493
- x: touch.pageX,
30494
- y: touch.pageY,
30495
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
30496
- screen
30557
+ x: start?.x ?? touch.pageX,
30558
+ y: start?.y ?? touch.pageY,
30559
+ hasTarget: !!(start?.targetId || start?.target),
30560
+ screen: start?.screenName
30497
30561
  });
30498
30562
  } catch {
30499
30563
  }
@@ -30506,11 +30570,9 @@ function ScaleBunDebugRoot({
30506
30570
  const capturedDetails = {
30507
30571
  ...gestureDetails
30508
30572
  };
30509
- if (pendingJsEmitRef.current !== null) {
30510
- clearTimeout(pendingJsEmitRef.current);
30511
- }
30512
- pendingJsEmitRef.current = setTimeout(() => {
30513
- pendingJsEmitRef.current = null;
30573
+ const interactionId = capturedDetails.interactionId;
30574
+ const timer = setTimeout(() => {
30575
+ pendingJsEmitsRef.current.delete(interactionId);
30514
30576
  if (nativeTrackingConfirmedRef.current) return;
30515
30577
  try {
30516
30578
  const {
@@ -30527,6 +30589,7 @@ function ScaleBunDebugRoot({
30527
30589
  } catch {
30528
30590
  }
30529
30591
  }, 600);
30592
+ pendingJsEmitsRef.current.set(interactionId, timer);
30530
30593
  }
30531
30594
  touchStartRef.current = null;
30532
30595
  } catch {
@@ -30602,20 +30665,6 @@ function useScaleBunScreen(name) {
30602
30665
  }
30603
30666
  }, [name, manager]);
30604
30667
  }
30605
- function _extractTarget(e) {
30606
- try {
30607
- const target = e?.target;
30608
- if (!target) return void 0;
30609
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
30610
- return {
30611
- testId: props.testID || props.nativeID || void 0,
30612
- accessibilityLabel: props.accessibilityLabel || void 0,
30613
- text: void 0
30614
- };
30615
- } catch {
30616
- return void 0;
30617
- }
30618
- }
30619
30668
  var styles4 = {
30620
30669
  root: {
30621
30670
  flex: 1