@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.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
- package/dist/scalebun.full.js +467 -255
- package/dist/scalebun.slim.js +466 -254
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/ios/Ota/OtaSlotManager.swift +19 -5
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +8 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
- package/lib/commonjs/features/ota/OtaTypes.js +4 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/commonjs/public/ScaleBunFacade.js +115 -2
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +8 -1
- package/lib/module/features/ota/OtaOrchestrator.js +174 -48
- package/lib/module/features/ota/OtaTypes.js +1 -1
- package/lib/module/features/ota/useOtaUpdate.js +11 -2
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/module/public/ScaleBunFacade.js +115 -2
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
- package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
- package/lib/typescript/features/session/SessionManager.d.ts +15 -10
- package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
- package/package.json +4 -3
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +9 -4
- package/src/features/ota/OtaEventEmitter.ts +12 -0
- package/src/features/ota/OtaOrchestrator.ts +209 -62
- package/src/features/ota/OtaTypes.ts +37 -39
- package/src/features/ota/useOtaUpdate.ts +11 -2
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
- 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
|
|
21
|
-
private
|
|
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
|
|
59
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
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: !!(
|
|
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
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|