@scalebun/react-native 1.10.6 → 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.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +75 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +12 -0
- package/dist/scalebun.full.js +653 -223
- package/dist/scalebun.slim.js +652 -222
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -0
- package/ios/Ota/ScaleBunOtaModule.swift +67 -0
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/config/schema.js +34 -1
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -79
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +94 -0
- package/lib/commonjs/features/ota/crypto/builtinVerifier.js +248 -0
- package/lib/commonjs/features/ota/crypto/loadEd25519.js +40 -0
- package/lib/commonjs/features/ota/crypto/loadSha512.js +40 -0
- package/lib/commonjs/features/ota/crypto/nativeVerifier.js +121 -0
- package/lib/commonjs/features/ota/signature.js +87 -27
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/commonjs/metro/serializerCompose.js +32 -0
- package/lib/commonjs/public/ScaleBunFacade.js +74 -11
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/config/schema.js +34 -1
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -79
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +86 -0
- package/lib/module/features/ota/crypto/builtinVerifier.js +240 -0
- package/lib/module/features/ota/crypto/loadEd25519.js +34 -0
- package/lib/module/features/ota/crypto/loadSha512.js +34 -0
- package/lib/module/features/ota/crypto/nativeVerifier.js +113 -0
- package/lib/module/features/ota/signature.js +87 -27
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/module/metro/serializerCompose.js +32 -0
- package/lib/module/public/ScaleBunFacade.js +74 -11
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/config/schema.d.ts +2 -0
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/journey/uiState.d.ts +53 -0
- package/lib/typescript/features/ota/OtaTypes.d.ts +50 -0
- package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +53 -0
- package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +30 -0
- package/lib/typescript/features/ota/crypto/loadSha512.d.ts +15 -0
- package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +35 -0
- package/lib/typescript/features/ota/signature.d.ts +22 -7
- 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 +35 -6
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +23 -0
- package/package.json +19 -3
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/config/schema.ts +30 -3
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -75
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +89 -0
- package/src/features/ota/OtaTypes.ts +51 -0
- package/src/features/ota/crypto/builtinVerifier.ts +257 -0
- package/src/features/ota/crypto/loadEd25519.ts +41 -0
- package/src/features/ota/crypto/loadSha512.ts +35 -0
- package/src/features/ota/crypto/nativeVerifier.ts +117 -0
- package/src/features/ota/signature.ts +108 -25
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
- package/src/metro/serializerCompose.ts +38 -2
- package/src/public/ScaleBunFacade.ts +87 -13
- package/src/specs/NativeScaleBunOta.ts +24 -0
|
@@ -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,
|
|
@@ -40,6 +40,12 @@ RCT_EXTERN_METHOD(revertToPrevious:(RCTPromiseResolveBlock)resolve
|
|
|
40
40
|
|
|
41
41
|
RCT_EXTERN_METHOD(restartApp)
|
|
42
42
|
|
|
43
|
+
RCT_EXTERN_METHOD(verifyEd25519:(NSString *)messageHex
|
|
44
|
+
signatureHex:(NSString *)signatureHex
|
|
45
|
+
publicKeyHex:(NSString *)publicKeyHex
|
|
46
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
47
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
48
|
+
|
|
43
49
|
@end
|
|
44
50
|
|
|
45
51
|
// Download-progress event channel. Swift subclass of RCTEventEmitter; only
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import React
|
|
3
3
|
import CommonCrypto
|
|
4
|
+
import CryptoKit
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* React Native bridge for OTA bundle management (`ScaleBunOta`) on iOS.
|
|
@@ -252,4 +253,70 @@ class ScaleBunOtaModule: NSObject, RCTBridgeModule {
|
|
|
252
253
|
RCTTriggerReloadCommandListeners("ScaleBun OTA update")
|
|
253
254
|
}
|
|
254
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Verify a detached ed25519 signature with PLATFORM crypto (CryptoKit).
|
|
259
|
+
*
|
|
260
|
+
* Why native: the JS verifier lives inside the very bundle it protects —
|
|
261
|
+
* one malicious bundle can neuter it for every update after. CryptoKit is
|
|
262
|
+
* outside the bundle's reach, and unlike Android there is no OS floor to
|
|
263
|
+
* worry about: Curve25519 signing shipped in iOS 13 and the podspec
|
|
264
|
+
* already requires 13.4.
|
|
265
|
+
*
|
|
266
|
+
* Contract (mirrors `src/specs/NativeScaleBunOta.ts`): inputs are
|
|
267
|
+
* lowercase hex pre-validated by the JS caller; the message is the RAW 32
|
|
268
|
+
* BYTES of the bundle SHA-256 (the CLI signs digest bytes, not hex text);
|
|
269
|
+
* resolves 'valid' / 'invalid' / 'unavailable'. A wrong signature is
|
|
270
|
+
* 'invalid', never a rejection; anything unexpected is 'unavailable' so
|
|
271
|
+
* machinery failure can never read as a pass.
|
|
272
|
+
*
|
|
273
|
+
* ⚪ UNCOMPILED — no macOS toolchain has been available in this workspace
|
|
274
|
+
* (the standing gap: BsPatch.swift shipped the same way). The algorithm is
|
|
275
|
+
* pinned by `tools/ed25519-parity` against the live production fixture;
|
|
276
|
+
* this file still needs one Xcode build before an iOS consumer ships it.
|
|
277
|
+
*/
|
|
278
|
+
@objc(verifyEd25519:signatureHex:publicKeyHex:resolve:reject:)
|
|
279
|
+
func verifyEd25519(_ messageHex: NSString,
|
|
280
|
+
signatureHex: NSString,
|
|
281
|
+
publicKeyHex: NSString,
|
|
282
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
283
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
284
|
+
guard let message = ScaleBunOtaModule.bytesFromHex(messageHex as String),
|
|
285
|
+
let signature = ScaleBunOtaModule.bytesFromHex(signatureHex as String),
|
|
286
|
+
let rawKey = ScaleBunOtaModule.bytesFromHex(publicKeyHex as String),
|
|
287
|
+
rawKey.count == 32 else {
|
|
288
|
+
// Malformed input from our own caller is not a forgery verdict.
|
|
289
|
+
resolve("unavailable")
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
do {
|
|
293
|
+
let key = try Curve25519.Signing.PublicKey(rawRepresentation: rawKey)
|
|
294
|
+
resolve(key.isValidSignature(signature, for: message) ? "valid" : "invalid")
|
|
295
|
+
} catch {
|
|
296
|
+
// Bytes that are not a curve point cannot have signed anything.
|
|
297
|
+
resolve("invalid")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Hex → Data; nil for odd length or a non-hex character.
|
|
302
|
+
private static func bytesFromHex(_ hex: String) -> Data? {
|
|
303
|
+
let chars = Array(hex.utf8)
|
|
304
|
+
guard !chars.isEmpty, chars.count % 2 == 0 else { return nil }
|
|
305
|
+
var out = Data(capacity: chars.count / 2)
|
|
306
|
+
for i in stride(from: 0, to: chars.count, by: 2) {
|
|
307
|
+
guard let hi = ScaleBunOtaModule.hexNibble(chars[i]),
|
|
308
|
+
let lo = ScaleBunOtaModule.hexNibble(chars[i + 1]) else { return nil }
|
|
309
|
+
out.append((hi << 4) | lo)
|
|
310
|
+
}
|
|
311
|
+
return out
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private static func hexNibble(_ c: UInt8) -> UInt8? {
|
|
315
|
+
switch c {
|
|
316
|
+
case 0x30...0x39: return c - 0x30 // 0-9
|
|
317
|
+
case 0x61...0x66: return c - 0x61 + 10 // a-f
|
|
318
|
+
case 0x41...0x46: return c - 0x41 + 10 // A-F
|
|
319
|
+
default: return nil
|
|
320
|
+
}
|
|
321
|
+
}
|
|
255
322
|
}
|
|
@@ -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);
|
|
@@ -295,6 +295,14 @@ const SHAPE = {
|
|
|
295
295
|
kind: 'str',
|
|
296
296
|
opt: true
|
|
297
297
|
},
|
|
298
|
+
// Rotation support: several pinned keys, any of which may verify a
|
|
299
|
+
// bundle. Ship a build pinning [old, new], re-sign server-side with
|
|
300
|
+
// new, drop old next release — no emergency store submission when a
|
|
301
|
+
// key must be replaced. Merged with publicSigningKey by the facade.
|
|
302
|
+
publicSigningKeys: {
|
|
303
|
+
kind: 'strArr',
|
|
304
|
+
opt: true
|
|
305
|
+
},
|
|
298
306
|
mandatoryBlocksUi: bool(false)
|
|
299
307
|
}
|
|
300
308
|
}
|
|
@@ -334,7 +342,7 @@ function parseField(field, input, path, issues) {
|
|
|
334
342
|
present: true
|
|
335
343
|
};
|
|
336
344
|
// Required with no default (matches the base schemas' type errors on undefined).
|
|
337
|
-
return fail(field.kind === 'bool' ? 'Expected boolean, received undefined' : field.kind === 'num' ? 'Expected number, received undefined' : field.kind === 'str' ? 'Expected string, received undefined' : field.kind === 'enum' ? "Invalid enum value. Expected " + field.values.map(v => `'${v}'`).join(' | ') : 'Expected object, received undefined');
|
|
345
|
+
return fail(field.kind === 'bool' ? 'Expected boolean, received undefined' : field.kind === 'num' ? 'Expected number, received undefined' : field.kind === 'str' ? 'Expected string, received undefined' : field.kind === 'strArr' ? 'Expected array, received undefined' : field.kind === 'enum' ? "Invalid enum value. Expected " + field.values.map(v => `'${v}'`).join(' | ') : 'Expected object, received undefined');
|
|
338
346
|
}
|
|
339
347
|
switch (field.kind) {
|
|
340
348
|
case 'any':
|
|
@@ -360,6 +368,31 @@ function parseField(field, input, path, issues) {
|
|
|
360
368
|
value: input,
|
|
361
369
|
present: true
|
|
362
370
|
};
|
|
371
|
+
case 'strArr':
|
|
372
|
+
{
|
|
373
|
+
// Mirrors the vendored ArraySchema exactly: element failures are
|
|
374
|
+
// reported at their index and fail the whole field (no partial
|
|
375
|
+
// arrays reach the output).
|
|
376
|
+
if (!Array.isArray(input)) return fail(`Expected array, received ${typeofName(input)}`);
|
|
377
|
+
let allOk = true;
|
|
378
|
+
for (let i = 0; i < input.length; i++) {
|
|
379
|
+
if (typeof input[i] !== 'string') {
|
|
380
|
+
issues.push({
|
|
381
|
+
path: [...path, i],
|
|
382
|
+
message: `Expected string, received ${typeofName(input[i])}`
|
|
383
|
+
});
|
|
384
|
+
allOk = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return allOk ? {
|
|
388
|
+
ok: true,
|
|
389
|
+
value: [...input],
|
|
390
|
+
present: true
|
|
391
|
+
} : {
|
|
392
|
+
ok: false,
|
|
393
|
+
present: true
|
|
394
|
+
};
|
|
395
|
+
}
|
|
363
396
|
case 'num':
|
|
364
397
|
if (typeof input !== 'number' || Number.isNaN(input)) {
|
|
365
398
|
return fail(`Expected number, received ${typeofName(input)}`);
|
|
@@ -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.0';
|
|
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,28 +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
|
-
(0, _automaticEvents.emitAutomaticEvent)('element_interacted', {
|
|
508
|
-
gesture_type: gestureType,
|
|
509
|
-
screen_name: screen,
|
|
510
|
-
// testID/nativeID is an author-controlled stable identifier.
|
|
511
|
-
// Accessibility labels and rendered text are deliberately omitted.
|
|
512
|
-
target_id: targetInfo?.testId,
|
|
513
|
-
normalized_x: gestureDetails.normalizedX,
|
|
514
|
-
normalized_y: gestureDetails.normalizedY,
|
|
515
|
-
direction: gestureDetails.direction,
|
|
516
|
-
duration_ms: gestureDetails.duration
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
547
|
|
|
520
548
|
// Feed the Engage gesture detector (rage_tap / dead_tap) — only committed
|
|
521
549
|
// taps, not swipes/scrolls/long-presses. Pure JS, cross-platform; emits the
|
|
@@ -525,18 +553,11 @@ function ScaleBunDebugRoot({
|
|
|
525
553
|
const {
|
|
526
554
|
gestureTriggerDetector
|
|
527
555
|
} = require('../engage/gestureTriggerDetector');
|
|
528
|
-
let screen;
|
|
529
|
-
try {
|
|
530
|
-
const {
|
|
531
|
-
AutoScreenDetector
|
|
532
|
-
} = require('../navigation/AutoScreenDetector');
|
|
533
|
-
screen = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
|
|
534
|
-
} catch {/* no-throw */}
|
|
535
556
|
gestureTriggerDetector.recordTap({
|
|
536
|
-
x: touch.pageX,
|
|
537
|
-
y: touch.pageY,
|
|
538
|
-
hasTarget: !!(
|
|
539
|
-
screen
|
|
557
|
+
x: start?.x ?? touch.pageX,
|
|
558
|
+
y: start?.y ?? touch.pageY,
|
|
559
|
+
hasTarget: !!(start?.targetId || start?.target),
|
|
560
|
+
screen: start?.screenName
|
|
540
561
|
});
|
|
541
562
|
} catch {/* no-throw */}
|
|
542
563
|
}
|
|
@@ -551,7 +572,7 @@ function ScaleBunDebugRoot({
|
|
|
551
572
|
// screenshot capture view). Three-tier suppression:
|
|
552
573
|
// 1. Native confirmed active → skip JS entirely (native handles all touches)
|
|
553
574
|
// 2. Native event within 300ms → skip (existing dedup)
|
|
554
|
-
// 3. First touch (native not yet confirmed) → defer
|
|
575
|
+
// 3. First touch (native not yet confirmed) → defer 600ms to let native arrive
|
|
555
576
|
const sinceNative = Date.now() - lastNativeTouchTsRef.current;
|
|
556
577
|
if (nativeTrackingConfirmedRef.current) {
|
|
557
578
|
// Native tracker is active — skip JS path entirely
|
|
@@ -564,11 +585,9 @@ function ScaleBunDebugRoot({
|
|
|
564
585
|
const capturedDetails = {
|
|
565
586
|
...gestureDetails
|
|
566
587
|
};
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
pendingJsEmitRef.current = setTimeout(() => {
|
|
571
|
-
pendingJsEmitRef.current = null;
|
|
588
|
+
const interactionId = capturedDetails.interactionId;
|
|
589
|
+
const timer = setTimeout(() => {
|
|
590
|
+
pendingJsEmitsRef.current.delete(interactionId);
|
|
572
591
|
if (nativeTrackingConfirmedRef.current) return;
|
|
573
592
|
try {
|
|
574
593
|
const {
|
|
@@ -592,6 +611,7 @@ function ScaleBunDebugRoot({
|
|
|
592
611
|
// devices with no native tracker at all it merely delays a background emission,
|
|
593
612
|
// which nothing user-visible waits on.
|
|
594
613
|
}, 600);
|
|
614
|
+
pendingJsEmitsRef.current.set(interactionId, timer);
|
|
595
615
|
}
|
|
596
616
|
touchStartRef.current = null;
|
|
597
617
|
} catch {/* no-throw */}
|
|
@@ -706,25 +726,6 @@ function useScaleBunScreen(name) {
|
|
|
706
726
|
}, [name, manager]);
|
|
707
727
|
}
|
|
708
728
|
|
|
709
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
710
|
-
|
|
711
|
-
function _extractTarget(e) {
|
|
712
|
-
try {
|
|
713
|
-
const target = e?.target;
|
|
714
|
-
if (!target) return undefined;
|
|
715
|
-
|
|
716
|
-
// React Native nativeEvent target properties
|
|
717
|
-
const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
|
|
718
|
-
return {
|
|
719
|
-
testId: props.testID || props.nativeID || undefined,
|
|
720
|
-
accessibilityLabel: props.accessibilityLabel || undefined,
|
|
721
|
-
text: undefined
|
|
722
|
-
};
|
|
723
|
-
} catch {
|
|
724
|
-
return undefined;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
729
|
// ─── Styles ─────────────────────────────────────────────────────────────────
|
|
729
730
|
// IMPORTANT: Do NOT use StyleSheet.create() here.
|
|
730
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
|