@scalebun/react-native 1.10.7 → 1.11.1

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 (54) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
  2. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
  3. package/dist/scalebun.full.js +467 -255
  4. package/dist/scalebun.slim.js +466 -254
  5. package/ios/Capture/InteractionTracker.swift +8 -4
  6. package/ios/Ota/OtaSlotManager.swift +19 -5
  7. package/lib/commonjs/analytics/EventTracker.js +5 -5
  8. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  9. package/lib/commonjs/core/constants/version.js +7 -2
  10. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
  11. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  12. package/lib/commonjs/features/journey/uiState.js +8 -1
  13. package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
  14. package/lib/commonjs/features/ota/OtaTypes.js +4 -0
  15. package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
  16. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  17. package/lib/commonjs/features/session/SessionManager.js +37 -38
  18. package/lib/commonjs/public/ScaleBunFacade.js +115 -2
  19. package/lib/module/analytics/EventTracker.js +5 -5
  20. package/lib/module/analytics/automaticEvents.js +3 -2
  21. package/lib/module/core/constants/version.js +7 -2
  22. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
  23. package/lib/module/features/journey/interactionProtocol.js +38 -0
  24. package/lib/module/features/journey/uiState.js +8 -1
  25. package/lib/module/features/ota/OtaOrchestrator.js +174 -48
  26. package/lib/module/features/ota/OtaTypes.js +1 -1
  27. package/lib/module/features/ota/useOtaUpdate.js +11 -2
  28. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  29. package/lib/module/features/session/SessionManager.js +37 -38
  30. package/lib/module/public/ScaleBunFacade.js +115 -2
  31. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  32. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  33. package/lib/typescript/core/constants/version.d.ts +7 -2
  34. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  35. package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
  36. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
  37. package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
  38. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  39. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  40. package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
  41. package/package.json +4 -3
  42. package/src/analytics/EventTracker.ts +5 -5
  43. package/src/analytics/automaticEvents.ts +4 -0
  44. package/src/core/constants/version.ts +7 -2
  45. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
  46. package/src/features/journey/interactionProtocol.ts +65 -0
  47. package/src/features/journey/uiState.ts +9 -4
  48. package/src/features/ota/OtaEventEmitter.ts +12 -0
  49. package/src/features/ota/OtaOrchestrator.ts +209 -62
  50. package/src/features/ota/OtaTypes.ts +37 -39
  51. package/src/features/ota/useOtaUpdate.ts +11 -2
  52. package/src/features/session/JourneyEventPipeline.ts +7 -5
  53. package/src/features/session/SessionManager.ts +75 -38
  54. package/src/public/ScaleBunFacade.ts +127 -3
@@ -17,8 +17,8 @@ class InteractionTracker: NSObject, UIGestureRecognizerDelegate {
17
17
 
18
18
  private var touchDownLocation: CGPoint = .zero
19
19
  private var touchDownTime: TimeInterval = 0
20
- private var lastEmitTime: TimeInterval = 0
21
- private let throttleInterval: TimeInterval = 0.15
20
+ private var touchInteractionId = ""
21
+ private var lastEmittedInteractionId = ""
22
22
 
23
23
  func attach(to window: UIWindow, sendEvent: @escaping (String, [String: Any]) -> Void) {
24
24
  guard !isActive else { return }
@@ -29,6 +29,7 @@ class InteractionTracker: NSObject, UIGestureRecognizerDelegate {
29
29
  onBegan: { [weak self] location in
30
30
  self?.touchDownLocation = location
31
31
  self?.touchDownTime = Date().timeIntervalSince1970
32
+ self?.touchInteractionId = "ixn-i-\(UUID().uuidString.lowercased())"
32
33
  },
33
34
  onEnded: { [weak self] location in
34
35
  self?.handleTouchEnded(at: location, in: window)
@@ -55,8 +56,8 @@ class InteractionTracker: NSObject, UIGestureRecognizerDelegate {
55
56
 
56
57
  private func handleTouchEnded(at location: CGPoint, in window: UIWindow) {
57
58
  let now = Date().timeIntervalSince1970
58
- guard now - lastEmitTime >= throttleInterval else { return }
59
- lastEmitTime = now
59
+ guard touchInteractionId != lastEmittedInteractionId else { return }
60
+ lastEmittedInteractionId = touchInteractionId
60
61
 
61
62
  let durationMs = (now - touchDownTime) * 1000
62
63
  let dx = location.x - touchDownLocation.x
@@ -104,6 +105,9 @@ class InteractionTracker: NSObject, UIGestureRecognizerDelegate {
104
105
 
105
106
  let event: [String: Any] = [
106
107
  "gestureType": gestureType,
108
+ "interactionId": touchInteractionId,
109
+ "interactionProtocol": 1,
110
+ "occurredAt": touchDownTime * 1000,
107
111
  "rawX": downInCapture.x,
108
112
  "rawY": downInCapture.y,
109
113
  "normalizedX": nX,
@@ -151,10 +151,19 @@ class OtaSlotManager {
151
151
  // it `bootGuardReverted` was read by the orchestrator and written by
152
152
  // nobody on iOS, so an auto-rollback that DID occur reported nothing —
153
153
  // the dashboard counted zero rollbacks while devices were reverting.
154
- let record: [String: Any] = [
154
+ // Stamp WHICH bundle is being reverted away from, read before current/ is
155
+ // removed below. Without it JS had nothing to name: slot meta carries no
156
+ // bundleId and the previous slot is gone by the time JS looks, so every
157
+ // AUTO_ROLLBACK reported bundleId "unknown" and the server dropped it as
158
+ // unresolvable — leaving the crash guard's rollback signal at zero.
159
+ var record: [String: Any] = [
155
160
  "reason": reason,
156
161
  "at": Int(Date().timeIntervalSince1970 * 1000),
157
162
  ]
163
+ if let meta = readMeta(slot: currentSlot),
164
+ let failingSha = meta["sha256"] as? String, !failingSha.isEmpty {
165
+ record["sha256"] = failingSha
166
+ }
158
167
  if let data = try? JSONSerialization.data(withJSONObject: record) {
159
168
  try? data.write(to: revertRecordFile, options: .atomic)
160
169
  }
@@ -195,14 +204,14 @@ class OtaSlotManager {
195
204
  let markerId = (markerJson["sha256"] as? String)
196
205
  ?? (markerJson["bundleId"] as? String) else {
197
206
  NSLog("[ScaleBunOta] Invalid boot marker — reverting")
198
- _ = revert()
207
+ _ = revert(reason: "invalid_marker")
199
208
  return
200
209
  }
201
210
 
202
211
  let metaURL = currentSlot.appendingPathComponent(OtaSlotManager.metaFilename)
203
212
  guard fm.fileExists(atPath: metaURL.path) else {
204
213
  NSLog("[ScaleBunOta] Boot marker present but no current/meta.json — reverting")
205
- _ = revert()
214
+ _ = revert(reason: "missing_meta")
206
215
  return
207
216
  }
208
217
 
@@ -210,7 +219,7 @@ class OtaSlotManager {
210
219
  guard let metaJson = try JSONSerialization.jsonObject(with: metaData) as? [String: Any],
211
220
  let currentId = metaJson["sha256"] as? String else {
212
221
  NSLog("[ScaleBunOta] Invalid current/meta.json — reverting")
213
- _ = revert()
222
+ _ = revert(reason: "invalid_meta")
214
223
  return
215
224
  }
216
225
 
@@ -250,7 +259,7 @@ class OtaSlotManager {
250
259
  }
251
260
  } catch {
252
261
  NSLog("[ScaleBunOta] checkBootGuard() failed — reverting: \(error)")
253
- _ = revert()
262
+ _ = revert(reason: "boot_guard_error")
254
263
  }
255
264
  }
256
265
 
@@ -273,6 +282,11 @@ class OtaSlotManager {
273
282
  state["bootGuardReverted"] = true
274
283
  state["bootGuardRevertReason"] = rec["reason"] as? String ?? "boot_guard"
275
284
  state["bootGuardRevertedAt"] = rec["at"] as? Int ?? 0
285
+ // The bundle reverted AWAY from. JS joins it back to a bundle id
286
+ // through its install record so the rollback can be reported.
287
+ if let sha = rec["sha256"] as? String, !sha.isEmpty {
288
+ state["bootGuardRevertedSha256"] = sha
289
+ }
276
290
  }
277
291
  try? fm.removeItem(at: revertRecordFile)
278
292
  }
@@ -103,7 +103,7 @@ class EventTracker {
103
103
  this.started = true;
104
104
  if (this.cfg.automaticEventTracking) {
105
105
  this.automaticEventsUnsubscribe = (0, _automaticEvents.subscribeAutomaticEvents)(event => {
106
- this.track(event.name, event.properties);
106
+ this.track(event.name, event.properties, event.timestamp);
107
107
  });
108
108
  }
109
109
  if (this.cfg.autoLifecycleEvents) {
@@ -226,9 +226,9 @@ class EventTracker {
226
226
 
227
227
  // ─── tracking ────────────────────────────────────────────────────────────
228
228
 
229
- track(eventName, properties) {
229
+ track(eventName, properties, timestamp) {
230
230
  try {
231
- this.enqueue(this.buildEnvelope(eventName, properties));
231
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
232
232
  try {
233
233
  this.cfg.onEvent?.(eventName);
234
234
  } catch {/* no-throw */}
@@ -365,7 +365,7 @@ class EventTracker {
365
365
 
366
366
  // ─── internals ─────────────────────────────────────────────────────────────
367
367
 
368
- buildEnvelope(eventName, properties) {
368
+ buildEnvelope(eventName, properties, timestamp) {
369
369
  const ctx = this.cfg.context ?? {};
370
370
  let canonicalSessionId;
371
371
  try {
@@ -376,7 +376,7 @@ class EventTracker {
376
376
  const env = {
377
377
  event_id: uuid(),
378
378
  event_name: eventName,
379
- event_time: Date.now(),
379
+ event_time: timestamp ?? Date.now(),
380
380
  app_id: this.cfg.appId,
381
381
  platform: this.cfg.platform ?? (0, _device.resolveEventPlatform)(),
382
382
  installation_id: this.installationId,
@@ -27,10 +27,11 @@ function compactProperties(properties) {
27
27
  }
28
28
  return out;
29
29
  }
30
- function emitAutomaticEvent(name, properties) {
30
+ function emitAutomaticEvent(name, properties, timestamp) {
31
31
  const event = {
32
32
  name,
33
- properties: compactProperties(properties)
33
+ properties: compactProperties(properties),
34
+ timestamp
34
35
  };
35
36
  if (listeners.size === 0) {
36
37
  pending.push(event);
@@ -7,7 +7,12 @@ exports.SDK_VERSION = void 0;
7
7
  /**
8
8
  * ScaleBun SDK version. Sent with the session-start envelope so the dashboard
9
9
  * can attribute telemetry to the SDK build that produced it.
10
- * Keep in sync with package.json "version".
10
+ * Keep in sync with package.json "version" — `version.test.ts` fails when they drift.
11
+ *
12
+ * Why the test matters: the 1.11.0 bump missed this line, so the build would have reported itself as
13
+ * 1.10.6. Every "is the release live, and on what share of traffic" question is answered from this
14
+ * value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
15
+ * version was introduced to solve.
11
16
  */
12
- const SDK_VERSION = exports.SDK_VERSION = '1.10.7';
17
+ const SDK_VERSION = exports.SDK_VERSION = '1.11.1';
13
18
  //# sourceMappingURL=version.js.map
@@ -8,9 +8,9 @@ exports.ScaleBunScreen = ScaleBunScreen;
8
8
  exports.useScaleBunScreen = useScaleBunScreen;
9
9
  var _react = _interopRequireWildcard(require("react"));
10
10
  var _reactNative = require("react-native");
11
- var _automaticEvents = require("../../analytics/automaticEvents");
12
11
  var _touchTarget = require("./touchTarget");
13
12
  var _device = require("../../core/context/device");
13
+ var _interactionProtocol = require("./interactionProtocol");
14
14
  var _jsxRuntime = require("react/jsx-runtime");
15
15
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
16
  /**
@@ -264,7 +264,9 @@ function ScaleBunDebugRoot({
264
264
  // and forwards them to SessionManager as USER_ACTION events.
265
265
  const lastNativeTouchTsRef = (0, _react.useRef)(0);
266
266
  const nativeTrackingConfirmedRef = (0, _react.useRef)(false);
267
- const pendingJsEmitRef = (0, _react.useRef)(null);
267
+ const touchStartRef = (0, _react.useRef)(null);
268
+ const interactionStartsRef = (0, _react.useRef)([]);
269
+ const pendingJsEmitsRef = (0, _react.useRef)(new Map());
268
270
  (0, _react.useEffect)(() => {
269
271
  let subscription = null;
270
272
  try {
@@ -276,11 +278,15 @@ function ScaleBunDebugRoot({
276
278
  // Record timestamp to suppress duplicate JS touch events
277
279
  lastNativeTouchTsRef.current = Date.now();
278
280
  nativeTrackingConfirmedRef.current = true;
279
-
280
- // Cancel any pending JS emission — native event wins
281
- if (pendingJsEmitRef.current !== null) {
282
- clearTimeout(pendingJsEmitRef.current);
283
- pendingJsEmitRef.current = null;
281
+ const nativeOccurredAt = typeof event.occurredAt === 'number' ? event.occurredAt : (typeof event.timestamp === 'number' ? event.timestamp : Date.now()) - (typeof event.durationMs === 'number' ? event.durationMs : 0);
282
+ const start = (0, _interactionProtocol.nearestInteractionStart)(interactionStartsRef.current, nativeOccurredAt);
283
+
284
+ // Cancel only this physical touch's fallback. A single timer used to cancel a
285
+ // previous rapid tap and silently lose it on devices without native tracking.
286
+ if (start) {
287
+ const pending = pendingJsEmitsRef.current.get(start.interactionId);
288
+ if (pending !== undefined) clearTimeout(pending);
289
+ pendingJsEmitsRef.current.delete(start.interactionId);
284
290
  }
285
291
  const {
286
292
  SessionManager
@@ -302,6 +308,15 @@ function ScaleBunDebugRoot({
302
308
  _diagWinH = _d.height;
303
309
  } catch {/* no-throw */}
304
310
  sm.onGestureDetected(event.gestureType || 'tap', {
311
+ interactionId: event.interactionId || start?.interactionId || (0, _interactionProtocol.generateInteractionId)(),
312
+ interactionProtocol: event.interactionProtocol || _interactionProtocol.INTERACTION_PROTOCOL_VERSION,
313
+ occurredAt: nativeOccurredAt,
314
+ ui: start?.ui,
315
+ stateStatus: start?.stateStatus ?? 'not_captured',
316
+ target: start?.target,
317
+ targetId: start?.targetId,
318
+ screenName: start?.screenName,
319
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
305
320
  x: event.rawX,
306
321
  y: event.rawY,
307
322
  // Use the actual native end coords when present, not the down
@@ -331,11 +346,12 @@ function ScaleBunDebugRoot({
331
346
  try {
332
347
  subscription?.remove();
333
348
  } catch {/* no-throw */}
349
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
350
+ pendingJsEmitsRef.current.clear();
334
351
  };
335
- }, []);
352
+ }, [captureAutomaticInteractions]);
336
353
 
337
354
  // ─── Touch Tracking State ───────────────────────────────────────────
338
- const touchStartRef = (0, _react.useRef)(null);
339
355
 
340
356
  // On-screen rect of the root view — the EXACT view the native screenshot
341
357
  // captures. Normalizing pageX/pageY against this rect makes JS-fallback taps
@@ -379,11 +395,37 @@ function ScaleBunDebugRoot({
379
395
  const handleTouchStart = e => {
380
396
  try {
381
397
  const touch = e.nativeEvent;
382
- touchStartRef.current = {
398
+ const target = (0, _touchTarget.resolveTouchTarget)(e);
399
+ let ui;
400
+ let screenName;
401
+ try {
402
+ const {
403
+ uiStateSignature
404
+ } = require('./uiState');
405
+ ui = uiStateSignature();
406
+ } catch {/* no-throw */}
407
+ try {
408
+ const {
409
+ AutoScreenDetector
410
+ } = require('../navigation/AutoScreenDetector');
411
+ screenName = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
412
+ } catch {/* no-throw */}
413
+ const start = {
414
+ interactionId: (0, _interactionProtocol.generateInteractionId)(),
415
+ occurredAt: Date.now(),
383
416
  x: touch.pageX,
384
417
  y: touch.pageY,
385
- ts: Date.now()
418
+ locationX: touch.locationX,
419
+ locationY: touch.locationY,
420
+ target: (0, _touchTarget.describeTouchTarget)(target),
421
+ targetId: target?.testID,
422
+ screenName,
423
+ ui,
424
+ stateStatus: ui ? 'captured_nonempty' : 'not_instrumented',
425
+ emitAutomaticAnalytics: captureAutomaticInteractions
386
426
  };
427
+ touchStartRef.current = start;
428
+ interactionStartsRef.current = interactionStartsRef.current.filter(candidate => start.occurredAt - candidate.occurredAt < 5000).concat(start).slice(-8);
387
429
  } catch {/* no-throw */}
388
430
  };
389
431
  const handleTouchEnd = e => {
@@ -422,15 +464,23 @@ function ScaleBunDebugRoot({
422
464
  // WHAT was tapped, resolved from the React fiber on the touch event. Without this the tap
423
465
  // carries only coordinates, so every tap in the app groups into one row per gesture type.
424
466
  // Defensive by construction — returns undefined rather than throwing (see touchTarget.ts).
425
- const tapped = (0, _touchTarget.describeTouchTarget)((0, _touchTarget.resolveTouchTarget)(e));
467
+ const tapped = start?.target ?? (0, _touchTarget.describeTouchTarget)((0, _touchTarget.resolveTouchTarget)(e));
426
468
  const gestureDetails = {
427
469
  target: tapped,
428
- x: touch.pageX,
429
- y: touch.pageY,
430
- pageX: touch.pageX,
431
- pageY: touch.pageY,
432
- locationX: touch.locationX,
433
- locationY: touch.locationY,
470
+ interactionId: start?.interactionId ?? (0, _interactionProtocol.generateInteractionId)(),
471
+ interactionProtocol: _interactionProtocol.INTERACTION_PROTOCOL_VERSION,
472
+ occurredAt: start?.occurredAt ?? Date.now(),
473
+ ui: start?.ui,
474
+ stateStatus: start?.stateStatus ?? 'not_captured',
475
+ targetId: start?.targetId,
476
+ screenName: start?.screenName,
477
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
478
+ x: start?.x ?? touch.pageX,
479
+ y: start?.y ?? touch.pageY,
480
+ pageX: start?.x ?? touch.pageX,
481
+ pageY: start?.y ?? touch.pageY,
482
+ locationX: start?.locationX ?? touch.locationX,
483
+ locationY: start?.locationY ?? touch.locationY,
434
484
  viewportWidth: vpW > 0 ? vpW : undefined,
435
485
  viewportHeight: vpH > 0 ? vpH : undefined
436
486
  };
@@ -442,8 +492,8 @@ function ScaleBunDebugRoot({
442
492
  // measureInWindow returns rootRect.y = statusBarHeight, so subtracting it
443
493
  // double-counts the status bar and pushes every marker upward.
444
494
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
445
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
446
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
495
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
496
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
447
497
  gestureDetails.normalizedX = nx;
448
498
  gestureDetails.normalizedY = ny;
449
499
  gestureDetails.normalizedPrecomputed = true;
@@ -475,7 +525,7 @@ function ScaleBunDebugRoot({
475
525
  const dx = touch.pageX - start.x;
476
526
  const dy = touch.pageY - start.y;
477
527
  const dist = Math.sqrt(dx * dx + dy * dy);
478
- const duration = Date.now() - start.ts;
528
+ const duration = Date.now() - start.occurredAt;
479
529
  gestureDetails.duration = duration;
480
530
  if (dist >= 15) {
481
531
  // Movement gesture: swipe or scroll
@@ -494,52 +544,6 @@ function ScaleBunDebugRoot({
494
544
  gestureType = 'long_press';
495
545
  }
496
546
  }
497
- const targetInfo = _extractTarget(e);
498
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
499
- if (captureAutomaticInteractions) {
500
- let screen;
501
- try {
502
- const {
503
- AutoScreenDetector
504
- } = require('../navigation/AutoScreenDetector');
505
- screen = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
506
- } catch {/* no-throw */}
507
- /**
508
- * UI STATE, read HERE and not later.
509
- *
510
- * A tap belongs to the surface that was on screen when the finger landed: tapping the
511
- * filter button while the sheet is DOWN belongs to `closed`, because that is what the
512
- * user was looking at when they reached for it. Reading it after the handler has run
513
- * moves every "open the thing" tap into the state it created — the one state it
514
- * certainly does not belong to.
515
- *
516
- * Declared-only on this platform (see uiState.ts): absent means not captured, which
517
- * the dashboard keeps distinct from "nothing was open".
518
- */
519
- let ui;
520
- try {
521
- const {
522
- uiStateSignature
523
- } = require('./uiState');
524
- ui = uiStateSignature();
525
- } catch {/* no-throw: a tap must never be lost to state capture */}
526
- (0, _automaticEvents.emitAutomaticEvent)('element_interacted', {
527
- gesture_type: gestureType,
528
- screen_name: screen,
529
- /* THIS MAP ENUMERATES. A field added to the payload and forgotten here reaches the
530
- backend as undefined with no error anywhere — the recurring defect class in this
531
- codebase. `ui` is the key the grid aggregate reads for state, byte-identical to
532
- the web SDK's, so one dashboard control queries both platforms. */
533
- ui,
534
- // testID/nativeID is an author-controlled stable identifier.
535
- // Accessibility labels and rendered text are deliberately omitted.
536
- target_id: targetInfo?.testId,
537
- normalized_x: gestureDetails.normalizedX,
538
- normalized_y: gestureDetails.normalizedY,
539
- direction: gestureDetails.direction,
540
- duration_ms: gestureDetails.duration
541
- });
542
- }
543
547
 
544
548
  // Feed the Engage gesture detector (rage_tap / dead_tap) — only committed
545
549
  // taps, not swipes/scrolls/long-presses. Pure JS, cross-platform; emits the
@@ -549,18 +553,11 @@ function ScaleBunDebugRoot({
549
553
  const {
550
554
  gestureTriggerDetector
551
555
  } = require('../engage/gestureTriggerDetector');
552
- let screen;
553
- try {
554
- const {
555
- AutoScreenDetector
556
- } = require('../navigation/AutoScreenDetector');
557
- screen = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
558
- } catch {/* no-throw */}
559
556
  gestureTriggerDetector.recordTap({
560
- x: touch.pageX,
561
- y: touch.pageY,
562
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
563
- screen
557
+ x: start?.x ?? touch.pageX,
558
+ y: start?.y ?? touch.pageY,
559
+ hasTarget: !!(start?.targetId || start?.target),
560
+ screen: start?.screenName
564
561
  });
565
562
  } catch {/* no-throw */}
566
563
  }
@@ -575,7 +572,7 @@ function ScaleBunDebugRoot({
575
572
  // screenshot capture view). Three-tier suppression:
576
573
  // 1. Native confirmed active → skip JS entirely (native handles all touches)
577
574
  // 2. Native event within 300ms → skip (existing dedup)
578
- // 3. First touch (native not yet confirmed) → defer 200ms to let native arrive
575
+ // 3. First touch (native not yet confirmed) → defer 600ms to let native arrive
579
576
  const sinceNative = Date.now() - lastNativeTouchTsRef.current;
580
577
  if (nativeTrackingConfirmedRef.current) {
581
578
  // Native tracker is active — skip JS path entirely
@@ -588,11 +585,9 @@ function ScaleBunDebugRoot({
588
585
  const capturedDetails = {
589
586
  ...gestureDetails
590
587
  };
591
- if (pendingJsEmitRef.current !== null) {
592
- clearTimeout(pendingJsEmitRef.current);
593
- }
594
- pendingJsEmitRef.current = setTimeout(() => {
595
- pendingJsEmitRef.current = null;
588
+ const interactionId = capturedDetails.interactionId;
589
+ const timer = setTimeout(() => {
590
+ pendingJsEmitsRef.current.delete(interactionId);
596
591
  if (nativeTrackingConfirmedRef.current) return;
597
592
  try {
598
593
  const {
@@ -616,6 +611,7 @@ function ScaleBunDebugRoot({
616
611
  // devices with no native tracker at all it merely delays a background emission,
617
612
  // which nothing user-visible waits on.
618
613
  }, 600);
614
+ pendingJsEmitsRef.current.set(interactionId, timer);
619
615
  }
620
616
  touchStartRef.current = null;
621
617
  } catch {/* no-throw */}
@@ -730,25 +726,6 @@ function useScaleBunScreen(name) {
730
726
  }, [name, manager]);
731
727
  }
732
728
 
733
- // ─── Helpers ────────────────────────────────────────────────────────────────
734
-
735
- function _extractTarget(e) {
736
- try {
737
- const target = e?.target;
738
- if (!target) return undefined;
739
-
740
- // React Native nativeEvent target properties
741
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
742
- return {
743
- testId: props.testID || props.nativeID || undefined,
744
- accessibilityLabel: props.accessibilityLabel || undefined,
745
- text: undefined
746
- };
747
- } catch {
748
- return undefined;
749
- }
750
- }
751
-
752
729
  // ─── Styles ─────────────────────────────────────────────────────────────────
753
730
  // IMPORTANT: Do NOT use StyleSheet.create() here.
754
731
  // In RN 0.84 bridgeless mode, StyleSheet.create() calls
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.INTERACTION_PROTOCOL_VERSION = void 0;
7
+ exports.automaticInteractionProperties = automaticInteractionProperties;
8
+ exports.generateInteractionId = generateInteractionId;
9
+ exports.nearestInteractionStart = nearestInteractionStart;
10
+ /** One physical interaction, shared by replay and analytics projections. */
11
+ const INTERACTION_PROTOCOL_VERSION = exports.INTERACTION_PROTOCOL_VERSION = 1;
12
+ function generateInteractionId() {
13
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
14
+ }
15
+
16
+ /** Match a native bridge event to the JS evidence sampled at the same finger-down. */
17
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
18
+ let best;
19
+ let bestDelta = toleranceMs + 1;
20
+ for (const start of starts) {
21
+ const delta = Math.abs(start.occurredAt - occurredAt);
22
+ if (delta < bestDelta) {
23
+ best = start;
24
+ bestDelta = delta;
25
+ }
26
+ }
27
+ return bestDelta <= toleranceMs ? best : undefined;
28
+ }
29
+
30
+ /** Analytics is a projection of the same evidence; no second click is invented. */
31
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
32
+ return {
33
+ gesture_type: payload.gestureType,
34
+ screen_name: screenName,
35
+ interaction_id: payload.interaction_id,
36
+ interaction_protocol: payload.interaction_protocol,
37
+ state_status: payload.state_status,
38
+ ui: payload.ui,
39
+ target_id: payload.target_id,
40
+ normalized_x: payload.normalizedX,
41
+ normalized_y: payload.normalizedY,
42
+ direction: payload.direction,
43
+ duration_ms: payload.durationMs,
44
+ canonical_mirror: canonicalMirror || undefined
45
+ };
46
+ }
47
+ //# sourceMappingURL=interactionProtocol.js.map
@@ -82,6 +82,13 @@ function uiStateSignature() {
82
82
  if (!declared.size) return undefined;
83
83
  /* SORTED, or the same surface produces different signatures depending on the order the host happened
84
84
  to declare things in, and every count fragments into several that mean nothing. */
85
- return [...declared.keys()].sort().map(k => `${k}:${declared.get(k)}`).join(';').slice(0, 96);
85
+ const pairs = [...declared.keys()].sort().map(k => `${k}:${declared.get(k)}`);
86
+ let signature = '';
87
+ for (const pair of pairs) {
88
+ const next = signature ? `${signature};${pair}` : pair;
89
+ if (next.length > 96) break;
90
+ signature = next;
91
+ }
92
+ return signature || undefined;
86
93
  }
87
94
  //# sourceMappingURL=uiState.js.map