sitepong 0.2.8 → 0.2.9
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/dist/entries/rn.d.ts +20 -0
- package/dist/entries/rn.js +1341 -1187
- package/dist/entries/rn.js.map +1 -1
- package/ios/WatchtowerCore/StructuralRecorder.swift +64 -5
- package/package.json +1 -1
package/dist/entries/rn.js
CHANGED
|
@@ -3315,600 +3315,6 @@ function SensitiveView(props) {
|
|
|
3315
3315
|
);
|
|
3316
3316
|
}
|
|
3317
3317
|
|
|
3318
|
-
// src/react-native/error-handler.ts
|
|
3319
|
-
function setupRNErrorHandler(options = {}) {
|
|
3320
|
-
const {
|
|
3321
|
-
captureFatal = true,
|
|
3322
|
-
captureNonFatal = true,
|
|
3323
|
-
capturePromiseRejections = true
|
|
3324
|
-
} = options;
|
|
3325
|
-
const teardowns = [];
|
|
3326
|
-
if (typeof ErrorUtils !== "undefined") {
|
|
3327
|
-
const previousHandler = ErrorUtils.getGlobalHandler();
|
|
3328
|
-
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
3329
|
-
if (isFatal && captureFatal || !isFatal && captureNonFatal) {
|
|
3330
|
-
captureError(error, {
|
|
3331
|
-
tags: {
|
|
3332
|
-
fatal: String(isFatal),
|
|
3333
|
-
handler: "react-native-global"
|
|
3334
|
-
}
|
|
3335
|
-
});
|
|
3336
|
-
flushScreenRecording().catch(() => {
|
|
3337
|
-
});
|
|
3338
|
-
}
|
|
3339
|
-
if (previousHandler) {
|
|
3340
|
-
previousHandler(error, isFatal);
|
|
3341
|
-
}
|
|
3342
|
-
});
|
|
3343
|
-
teardowns.push(() => {
|
|
3344
|
-
if (previousHandler) {
|
|
3345
|
-
ErrorUtils.setGlobalHandler(previousHandler);
|
|
3346
|
-
}
|
|
3347
|
-
});
|
|
3348
|
-
}
|
|
3349
|
-
if (capturePromiseRejections) {
|
|
3350
|
-
if (typeof global !== "undefined") {
|
|
3351
|
-
const rejectionHandler = (event) => {
|
|
3352
|
-
const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
3353
|
-
captureError(err, {
|
|
3354
|
-
tags: { handler: "unhandled-promise-rejection" }
|
|
3355
|
-
});
|
|
3356
|
-
flushScreenRecording().catch(() => {
|
|
3357
|
-
});
|
|
3358
|
-
};
|
|
3359
|
-
if (typeof globalThis.addEventListener === "function") {
|
|
3360
|
-
globalThis.addEventListener("unhandledrejection", rejectionHandler);
|
|
3361
|
-
teardowns.push(() => {
|
|
3362
|
-
globalThis.removeEventListener("unhandledrejection", rejectionHandler);
|
|
3363
|
-
});
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
return () => {
|
|
3368
|
-
for (const teardown of teardowns) {
|
|
3369
|
-
teardown();
|
|
3370
|
-
}
|
|
3371
|
-
};
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
// src/react-native/navigation.ts
|
|
3375
|
-
function createNavigationTracker(navigationRef, options = {}) {
|
|
3376
|
-
const {
|
|
3377
|
-
trackScreenViews = true,
|
|
3378
|
-
addBreadcrumbs = true,
|
|
3379
|
-
screenNameFilter
|
|
3380
|
-
} = options;
|
|
3381
|
-
let currentRouteName;
|
|
3382
|
-
const unsubscribe = navigationRef.addListener("state", () => {
|
|
3383
|
-
const route = navigationRef.getCurrentRoute();
|
|
3384
|
-
if (!route) return;
|
|
3385
|
-
const previousRouteName = currentRouteName;
|
|
3386
|
-
currentRouteName = route.name;
|
|
3387
|
-
if (previousRouteName === currentRouteName) return;
|
|
3388
|
-
const screenName = screenNameFilter ? screenNameFilter(currentRouteName) : currentRouteName;
|
|
3389
|
-
if (!screenName) return;
|
|
3390
|
-
setCurrentScreen(screenName);
|
|
3391
|
-
if (trackScreenViews) {
|
|
3392
|
-
track("$screen_view", {
|
|
3393
|
-
screen_name: screenName,
|
|
3394
|
-
previous_screen: previousRouteName,
|
|
3395
|
-
params: route.params
|
|
3396
|
-
});
|
|
3397
|
-
}
|
|
3398
|
-
if (addBreadcrumbs) {
|
|
3399
|
-
addBreadcrumb({
|
|
3400
|
-
type: "navigation",
|
|
3401
|
-
category: "screen",
|
|
3402
|
-
message: `Navigated to ${screenName}`,
|
|
3403
|
-
data: {
|
|
3404
|
-
from: previousRouteName,
|
|
3405
|
-
to: screenName
|
|
3406
|
-
}
|
|
3407
|
-
});
|
|
3408
|
-
}
|
|
3409
|
-
});
|
|
3410
|
-
return () => {
|
|
3411
|
-
unsubscribe();
|
|
3412
|
-
setCurrentScreen(void 0);
|
|
3413
|
-
};
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
// src/react-native/network.ts
|
|
3417
|
-
function setupNetworkInterception(options = {}) {
|
|
3418
|
-
const {
|
|
3419
|
-
addBreadcrumbs = true,
|
|
3420
|
-
ignoreUrls = [/sitepong\.com/],
|
|
3421
|
-
maxDataSize = 1024
|
|
3422
|
-
} = options;
|
|
3423
|
-
if (typeof XMLHttpRequest === "undefined") {
|
|
3424
|
-
return () => {
|
|
3425
|
-
};
|
|
3426
|
-
}
|
|
3427
|
-
const originalOpen = XMLHttpRequest.prototype.open;
|
|
3428
|
-
const originalSend = XMLHttpRequest.prototype.send;
|
|
3429
|
-
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
3430
|
-
const xhr = this;
|
|
3431
|
-
xhr._sitepong_method = method;
|
|
3432
|
-
xhr._sitepong_url = String(url);
|
|
3433
|
-
return originalOpen.apply(this, [method, url, ...rest]);
|
|
3434
|
-
};
|
|
3435
|
-
XMLHttpRequest.prototype.send = function(body) {
|
|
3436
|
-
const xhr = this;
|
|
3437
|
-
const url = xhr._sitepong_url || "";
|
|
3438
|
-
const method = xhr._sitepong_method || "GET";
|
|
3439
|
-
const shouldIgnore = ignoreUrls.some((pattern) => {
|
|
3440
|
-
if (typeof pattern === "string") return url.includes(pattern);
|
|
3441
|
-
return pattern.test(url);
|
|
3442
|
-
});
|
|
3443
|
-
if (shouldIgnore) {
|
|
3444
|
-
return originalSend.call(this, body);
|
|
3445
|
-
}
|
|
3446
|
-
xhr._sitepong_startTime = Date.now();
|
|
3447
|
-
const onLoadEnd = () => {
|
|
3448
|
-
const duration = xhr._sitepong_startTime ? Date.now() - xhr._sitepong_startTime : void 0;
|
|
3449
|
-
if (addBreadcrumbs) {
|
|
3450
|
-
addBreadcrumb({
|
|
3451
|
-
type: "http",
|
|
3452
|
-
category: "xhr",
|
|
3453
|
-
message: `${method} ${url}`,
|
|
3454
|
-
level: xhr.status >= 400 ? "error" : "info",
|
|
3455
|
-
data: {
|
|
3456
|
-
method,
|
|
3457
|
-
url: url.length > maxDataSize ? url.substring(0, maxDataSize) : url,
|
|
3458
|
-
status_code: xhr.status,
|
|
3459
|
-
duration_ms: duration
|
|
3460
|
-
}
|
|
3461
|
-
});
|
|
3462
|
-
}
|
|
3463
|
-
};
|
|
3464
|
-
this.addEventListener("loadend", onLoadEnd);
|
|
3465
|
-
return originalSend.call(this, body);
|
|
3466
|
-
};
|
|
3467
|
-
return () => {
|
|
3468
|
-
XMLHttpRequest.prototype.open = originalOpen;
|
|
3469
|
-
XMLHttpRequest.prototype.send = originalSend;
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
var RNAutocaptureModule = class {
|
|
3473
|
-
constructor(options = {}) {
|
|
3474
|
-
this.appStateSubscription = null;
|
|
3475
|
-
this.currentAppState = "active";
|
|
3476
|
-
this.options = {
|
|
3477
|
-
trackAppState: options.trackAppState ?? true
|
|
3478
|
-
};
|
|
3479
|
-
}
|
|
3480
|
-
start() {
|
|
3481
|
-
if (this.options.trackAppState) {
|
|
3482
|
-
this.setupAppStateTracking();
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
stop() {
|
|
3486
|
-
if (this.appStateSubscription) {
|
|
3487
|
-
this.appStateSubscription.remove();
|
|
3488
|
-
this.appStateSubscription = null;
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
setupAppStateTracking() {
|
|
3492
|
-
try {
|
|
3493
|
-
this.currentAppState = reactNative.AppState.currentState;
|
|
3494
|
-
this.appStateSubscription = reactNative.AppState.addEventListener("change", (nextAppState) => {
|
|
3495
|
-
const previousState = this.currentAppState;
|
|
3496
|
-
this.currentAppState = nextAppState;
|
|
3497
|
-
if (previousState !== nextAppState) {
|
|
3498
|
-
track("$app_state_change", {
|
|
3499
|
-
from: previousState,
|
|
3500
|
-
to: nextAppState
|
|
3501
|
-
});
|
|
3502
|
-
if (previousState.match(/inactive|background/) && nextAppState === "active") {
|
|
3503
|
-
track("$app_foreground", {
|
|
3504
|
-
previous_state: previousState
|
|
3505
|
-
});
|
|
3506
|
-
} else if (previousState === "active" && nextAppState.match(/inactive|background/)) {
|
|
3507
|
-
track("$app_background", {
|
|
3508
|
-
next_state: nextAppState
|
|
3509
|
-
});
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
});
|
|
3513
|
-
} catch {
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
};
|
|
3517
|
-
|
|
3518
|
-
// src/react-native/performance.ts
|
|
3519
|
-
var coldStartTimestamp = null;
|
|
3520
|
-
var coldStartTracked = false;
|
|
3521
|
-
function markColdStart() {
|
|
3522
|
-
coldStartTimestamp = Date.now();
|
|
3523
|
-
}
|
|
3524
|
-
var RNPerformanceManager = class {
|
|
3525
|
-
constructor() {
|
|
3526
|
-
this.screenRenderStarts = /* @__PURE__ */ new Map();
|
|
3527
|
-
}
|
|
3528
|
-
/**
|
|
3529
|
-
* Track cold start time from markColdStart() to now.
|
|
3530
|
-
* Should be called once the app is interactive (e.g., in the root component's useEffect).
|
|
3531
|
-
*/
|
|
3532
|
-
trackColdStart() {
|
|
3533
|
-
if (coldStartTracked || !coldStartTimestamp) return;
|
|
3534
|
-
const duration = Date.now() - coldStartTimestamp;
|
|
3535
|
-
coldStartTracked = true;
|
|
3536
|
-
track("$cold_start", {
|
|
3537
|
-
duration_ms: duration
|
|
3538
|
-
});
|
|
3539
|
-
}
|
|
3540
|
-
/**
|
|
3541
|
-
* Start tracking a screen render. Call before rendering starts.
|
|
3542
|
-
*/
|
|
3543
|
-
startScreenRender(screenName) {
|
|
3544
|
-
this.screenRenderStarts.set(screenName, Date.now());
|
|
3545
|
-
}
|
|
3546
|
-
/**
|
|
3547
|
-
* End tracking a screen render. Call after rendering completes.
|
|
3548
|
-
*/
|
|
3549
|
-
endScreenRender(screenName) {
|
|
3550
|
-
const startTime = this.screenRenderStarts.get(screenName);
|
|
3551
|
-
if (startTime === void 0) return null;
|
|
3552
|
-
this.screenRenderStarts.delete(screenName);
|
|
3553
|
-
const duration = Date.now() - startTime;
|
|
3554
|
-
track("$screen_render", {
|
|
3555
|
-
screen_name: screenName,
|
|
3556
|
-
duration_ms: duration
|
|
3557
|
-
});
|
|
3558
|
-
return duration;
|
|
3559
|
-
}
|
|
3560
|
-
/**
|
|
3561
|
-
* Track a screen render using a useEffect-friendly pattern.
|
|
3562
|
-
* Returns a cleanup function.
|
|
3563
|
-
*/
|
|
3564
|
-
trackScreenRender(screenName) {
|
|
3565
|
-
this.startScreenRender(screenName);
|
|
3566
|
-
return () => {
|
|
3567
|
-
this.endScreenRender(screenName);
|
|
3568
|
-
};
|
|
3569
|
-
}
|
|
3570
|
-
};
|
|
3571
|
-
|
|
3572
|
-
// src/react-native/push.ts
|
|
3573
|
-
var cachedDeviceContext = null;
|
|
3574
|
-
function getEndpoint() {
|
|
3575
|
-
const cfg = sitepong.config;
|
|
3576
|
-
return cfg?.endpoint || "https://ingest.sitepong.com";
|
|
3577
|
-
}
|
|
3578
|
-
function getApiKey() {
|
|
3579
|
-
const cfg = sitepong.config;
|
|
3580
|
-
return cfg?.apiKey || "";
|
|
3581
|
-
}
|
|
3582
|
-
function getDeviceContext() {
|
|
3583
|
-
if (cachedDeviceContext) return cachedDeviceContext;
|
|
3584
|
-
try {
|
|
3585
|
-
const info = collectDeviceInfo();
|
|
3586
|
-
cachedDeviceContext = {
|
|
3587
|
-
platform: info.platform,
|
|
3588
|
-
appVersion: info.appVersion,
|
|
3589
|
-
deviceModel: info.deviceModel,
|
|
3590
|
-
osVersion: info.osVersion
|
|
3591
|
-
};
|
|
3592
|
-
fetchPersistentDeviceId().then((id) => {
|
|
3593
|
-
if (id && cachedDeviceContext) cachedDeviceContext.deviceId = id;
|
|
3594
|
-
});
|
|
3595
|
-
} catch {
|
|
3596
|
-
cachedDeviceContext = {};
|
|
3597
|
-
}
|
|
3598
|
-
return cachedDeviceContext;
|
|
3599
|
-
}
|
|
3600
|
-
async function postToIngest(path, body) {
|
|
3601
|
-
const endpoint = getEndpoint();
|
|
3602
|
-
const apiKey = getApiKey();
|
|
3603
|
-
if (!apiKey) {
|
|
3604
|
-
console.warn("[SitePong] Cannot register push token: SDK not initialized");
|
|
3605
|
-
return;
|
|
3606
|
-
}
|
|
3607
|
-
try {
|
|
3608
|
-
const response = await fetch(`${endpoint}${path}`, {
|
|
3609
|
-
method: "POST",
|
|
3610
|
-
headers: {
|
|
3611
|
-
"Content-Type": "application/json",
|
|
3612
|
-
"X-API-Key": apiKey
|
|
3613
|
-
},
|
|
3614
|
-
body: JSON.stringify(body)
|
|
3615
|
-
});
|
|
3616
|
-
if (!response.ok) {
|
|
3617
|
-
console.warn(`[SitePong] Push token registration failed: HTTP ${response.status}`);
|
|
3618
|
-
}
|
|
3619
|
-
} catch (err) {
|
|
3620
|
-
console.warn("[SitePong] Push token registration failed:", err);
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
async function deleteFromIngest(path, body) {
|
|
3624
|
-
const endpoint = getEndpoint();
|
|
3625
|
-
const apiKey = getApiKey();
|
|
3626
|
-
if (!apiKey) return;
|
|
3627
|
-
try {
|
|
3628
|
-
const response = await fetch(`${endpoint}${path}`, {
|
|
3629
|
-
method: "DELETE",
|
|
3630
|
-
headers: {
|
|
3631
|
-
"Content-Type": "application/json",
|
|
3632
|
-
"X-API-Key": apiKey
|
|
3633
|
-
},
|
|
3634
|
-
body: JSON.stringify(body)
|
|
3635
|
-
});
|
|
3636
|
-
if (!response.ok) {
|
|
3637
|
-
console.warn(`[SitePong] Push token deactivation failed: HTTP ${response.status}`);
|
|
3638
|
-
}
|
|
3639
|
-
} catch (err) {
|
|
3640
|
-
console.warn("[SitePong] Push token deactivation failed:", err);
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
3643
|
-
function registerPushToken(token, options) {
|
|
3644
|
-
const device = getDeviceContext();
|
|
3645
|
-
postToIngest("/api/push/tokens", {
|
|
3646
|
-
expo_push_token: token,
|
|
3647
|
-
environment: options.environment,
|
|
3648
|
-
device_id: device.deviceId,
|
|
3649
|
-
platform: device.platform,
|
|
3650
|
-
app_version: device.appVersion,
|
|
3651
|
-
device_model: device.deviceModel,
|
|
3652
|
-
os_version: device.osVersion
|
|
3653
|
-
});
|
|
3654
|
-
}
|
|
3655
|
-
function registerDeviceToken(token, options) {
|
|
3656
|
-
const device = getDeviceContext();
|
|
3657
|
-
const tokenType = options.platform === "ios" ? "apns" : "fcm";
|
|
3658
|
-
postToIngest("/api/push/tokens", {
|
|
3659
|
-
native_device_token: token,
|
|
3660
|
-
token_type: tokenType,
|
|
3661
|
-
environment: options.environment,
|
|
3662
|
-
device_id: device.deviceId,
|
|
3663
|
-
platform: options.platform,
|
|
3664
|
-
app_version: device.appVersion,
|
|
3665
|
-
device_model: device.deviceModel,
|
|
3666
|
-
os_version: device.osVersion
|
|
3667
|
-
});
|
|
3668
|
-
}
|
|
3669
|
-
function registerLiveActivityToken(activityType, activityId, pushToken, options) {
|
|
3670
|
-
const device = getDeviceContext();
|
|
3671
|
-
postToIngest("/api/push/live-activity-tokens", {
|
|
3672
|
-
activity_type: activityType,
|
|
3673
|
-
activity_id: activityId,
|
|
3674
|
-
push_token: pushToken,
|
|
3675
|
-
environment: options.environment,
|
|
3676
|
-
device_id: device.deviceId
|
|
3677
|
-
});
|
|
3678
|
-
}
|
|
3679
|
-
function registerPushToStartToken(activityType, pushToStartToken, options) {
|
|
3680
|
-
const device = getDeviceContext();
|
|
3681
|
-
postToIngest("/api/push/push-to-start-tokens", {
|
|
3682
|
-
activity_type: activityType,
|
|
3683
|
-
push_to_start_token: pushToStartToken,
|
|
3684
|
-
environment: options.environment,
|
|
3685
|
-
device_id: device.deviceId
|
|
3686
|
-
});
|
|
3687
|
-
}
|
|
3688
|
-
function endLiveActivity(activityType, activityId) {
|
|
3689
|
-
deleteFromIngest("/api/push/live-activity-tokens", {
|
|
3690
|
-
activity_type: activityType,
|
|
3691
|
-
activity_id: activityId
|
|
3692
|
-
});
|
|
3693
|
-
}
|
|
3694
|
-
var SitePongRNContext = React2.createContext({
|
|
3695
|
-
isInitialized: false,
|
|
3696
|
-
performanceManager: null
|
|
3697
|
-
});
|
|
3698
|
-
function SitePongRNProvider({
|
|
3699
|
-
apiKey,
|
|
3700
|
-
config = {},
|
|
3701
|
-
asyncStorage,
|
|
3702
|
-
navigationRef,
|
|
3703
|
-
trackNetwork = true,
|
|
3704
|
-
trackAppState = true,
|
|
3705
|
-
captureErrors = true,
|
|
3706
|
-
screenRecording,
|
|
3707
|
-
devicePushToken,
|
|
3708
|
-
devicePlatform,
|
|
3709
|
-
pushEnvironment = "production",
|
|
3710
|
-
pushUserId,
|
|
3711
|
-
children
|
|
3712
|
-
}) {
|
|
3713
|
-
const initialized = React2.useRef(false);
|
|
3714
|
-
const [sdkReady, setSdkReady] = React2.useState(false);
|
|
3715
|
-
const performanceManagerRef = React2.useRef(null);
|
|
3716
|
-
React2.useEffect(() => {
|
|
3717
|
-
if (initialized.current) return;
|
|
3718
|
-
const teardowns = [];
|
|
3719
|
-
const deviceInfo = collectDeviceInfo();
|
|
3720
|
-
setRNDeviceInfo({
|
|
3721
|
-
platform: "react-native",
|
|
3722
|
-
osName: deviceInfo.platform,
|
|
3723
|
-
osVersion: deviceInfo.osVersion,
|
|
3724
|
-
deviceName: deviceInfo.deviceName,
|
|
3725
|
-
deviceBrand: deviceInfo.deviceBrand,
|
|
3726
|
-
deviceModel: deviceInfo.deviceModel,
|
|
3727
|
-
appVersion: deviceInfo.appVersion,
|
|
3728
|
-
screenWidth: deviceInfo.screenWidth,
|
|
3729
|
-
screenHeight: deviceInfo.screenHeight,
|
|
3730
|
-
isEmulator: deviceInfo.isEmulator
|
|
3731
|
-
});
|
|
3732
|
-
fetchPersistentDeviceId().then((id) => {
|
|
3733
|
-
if (id) setNativeDeviceId(id);
|
|
3734
|
-
}).catch(() => {
|
|
3735
|
-
});
|
|
3736
|
-
const storageAdapter = asyncStorage ? createAsyncStorageAdapter(asyncStorage) : void 0;
|
|
3737
|
-
initRN({
|
|
3738
|
-
apiKey,
|
|
3739
|
-
...config,
|
|
3740
|
-
_storageAdapter: storageAdapter
|
|
3741
|
-
});
|
|
3742
|
-
if (captureErrors) {
|
|
3743
|
-
const teardownErrors = setupRNErrorHandler();
|
|
3744
|
-
teardowns.push(teardownErrors);
|
|
3745
|
-
}
|
|
3746
|
-
if (trackNetwork) {
|
|
3747
|
-
const teardownNetwork = setupNetworkInterception();
|
|
3748
|
-
teardowns.push(teardownNetwork);
|
|
3749
|
-
}
|
|
3750
|
-
if (trackAppState) {
|
|
3751
|
-
const autocapture = new RNAutocaptureModule({ trackAppState: true });
|
|
3752
|
-
autocapture.start();
|
|
3753
|
-
teardowns.push(() => autocapture.stop());
|
|
3754
|
-
}
|
|
3755
|
-
if (navigationRef) {
|
|
3756
|
-
const teardownNav = createNavigationTracker(navigationRef);
|
|
3757
|
-
teardowns.push(teardownNav);
|
|
3758
|
-
}
|
|
3759
|
-
if (screenRecording?.enabled) {
|
|
3760
|
-
const sessionId = `rn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
3761
|
-
const endpoint = config.endpoint || "https://ingest.sitepong.com";
|
|
3762
|
-
startScreenRecordingOnError(apiKey, endpoint, sessionId, screenRecording);
|
|
3763
|
-
teardowns.push(() => stopScreenRecording());
|
|
3764
|
-
}
|
|
3765
|
-
if (devicePushToken && devicePlatform) {
|
|
3766
|
-
registerDeviceToken(devicePushToken, { platform: devicePlatform, environment: pushEnvironment});
|
|
3767
|
-
}
|
|
3768
|
-
performanceManagerRef.current = new RNPerformanceManager();
|
|
3769
|
-
performanceManagerRef.current.trackColdStart();
|
|
3770
|
-
initialized.current = true;
|
|
3771
|
-
setSdkReady(true);
|
|
3772
|
-
return () => {
|
|
3773
|
-
for (const teardown of teardowns) {
|
|
3774
|
-
teardown();
|
|
3775
|
-
}
|
|
3776
|
-
};
|
|
3777
|
-
}, [apiKey]);
|
|
3778
|
-
const value = {
|
|
3779
|
-
isInitialized: sdkReady,
|
|
3780
|
-
performanceManager: performanceManagerRef.current
|
|
3781
|
-
};
|
|
3782
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SitePongRNContext.Provider, { value, children });
|
|
3783
|
-
}
|
|
3784
|
-
function useSitePongRNContext() {
|
|
3785
|
-
return React2.useContext(SitePongRNContext);
|
|
3786
|
-
}
|
|
3787
|
-
function useScreenTrack(screenName) {
|
|
3788
|
-
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
3789
|
-
React2.useEffect(() => {
|
|
3790
|
-
if (!performanceManager) return;
|
|
3791
|
-
performanceManager.startScreenRender(screenName);
|
|
3792
|
-
return () => {
|
|
3793
|
-
performanceManager.endScreenRender(screenName);
|
|
3794
|
-
};
|
|
3795
|
-
}, [screenName, performanceManager]);
|
|
3796
|
-
}
|
|
3797
|
-
function useAppState() {
|
|
3798
|
-
const [appState, setAppState] = React2.useState("active");
|
|
3799
|
-
React2.useEffect(() => {
|
|
3800
|
-
setAppState(reactNative.AppState.currentState);
|
|
3801
|
-
const subscription = reactNative.AppState.addEventListener("change", (nextState) => {
|
|
3802
|
-
setAppState(nextState);
|
|
3803
|
-
});
|
|
3804
|
-
return () => subscription.remove();
|
|
3805
|
-
}, []);
|
|
3806
|
-
return appState;
|
|
3807
|
-
}
|
|
3808
|
-
function useRemoteConfig() {
|
|
3809
|
-
const [config, setConfig] = React2.useState(() => getRemoteConfig());
|
|
3810
|
-
React2.useEffect(() => {
|
|
3811
|
-
setConfig(getRemoteConfig());
|
|
3812
|
-
const unsubscribe = onRemoteConfigChange((newConfig) => {
|
|
3813
|
-
setConfig(newConfig);
|
|
3814
|
-
});
|
|
3815
|
-
return unsubscribe;
|
|
3816
|
-
}, []);
|
|
3817
|
-
return config;
|
|
3818
|
-
}
|
|
3819
|
-
function useRNPerformance(screenName) {
|
|
3820
|
-
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
3821
|
-
const [duration, setDuration] = React2.useState(null);
|
|
3822
|
-
const startTimeRef = React2.useRef(Date.now());
|
|
3823
|
-
React2.useEffect(() => {
|
|
3824
|
-
startTimeRef.current = Date.now();
|
|
3825
|
-
const handle = requestAnimationFrame(() => {
|
|
3826
|
-
const d = Date.now() - startTimeRef.current;
|
|
3827
|
-
setDuration(d);
|
|
3828
|
-
if (performanceManager) {
|
|
3829
|
-
performanceManager.startScreenRender(screenName);
|
|
3830
|
-
performanceManager.endScreenRender(screenName);
|
|
3831
|
-
}
|
|
3832
|
-
});
|
|
3833
|
-
return () => cancelAnimationFrame(handle);
|
|
3834
|
-
}, [screenName, performanceManager]);
|
|
3835
|
-
return { duration };
|
|
3836
|
-
}
|
|
3837
|
-
var warnedMissing = false;
|
|
3838
|
-
function loadBridge() {
|
|
3839
|
-
return getScreenRecorderModule();
|
|
3840
|
-
}
|
|
3841
|
-
var started = false;
|
|
3842
|
-
function startWatchtower(config) {
|
|
3843
|
-
if (started) return;
|
|
3844
|
-
const bridge = loadBridge();
|
|
3845
|
-
if (!bridge) {
|
|
3846
|
-
if (!warnedMissing) {
|
|
3847
|
-
warnedMissing = true;
|
|
3848
|
-
console.warn(
|
|
3849
|
-
"[SitePong] Watchtower is enabled but the native module is unavailable. Rebuild the native app (npx expo prebuild && npx expo run:ios) so the bundled SitePong native module is linked."
|
|
3850
|
-
);
|
|
3851
|
-
}
|
|
3852
|
-
return;
|
|
3853
|
-
}
|
|
3854
|
-
bridge.watchtowerStart({
|
|
3855
|
-
apiKey: config.apiKey,
|
|
3856
|
-
projectId: config.projectId,
|
|
3857
|
-
endpoint: config.endpoint,
|
|
3858
|
-
sampleRate: config.sampleRate ?? 0.1,
|
|
3859
|
-
platform: config.platform ?? "react-native-ios"
|
|
3860
|
-
});
|
|
3861
|
-
started = true;
|
|
3862
|
-
if (config.distinctId) setWatchtowerUser(config.distinctId);
|
|
3863
|
-
}
|
|
3864
|
-
function setWatchtowerUser(distinctId) {
|
|
3865
|
-
try {
|
|
3866
|
-
loadBridge()?.watchtowerSetUser(distinctId);
|
|
3867
|
-
} catch {
|
|
3868
|
-
}
|
|
3869
|
-
}
|
|
3870
|
-
function getWatchtowerSessionId() {
|
|
3871
|
-
try {
|
|
3872
|
-
return loadBridge()?.watchtowerGetSessionId() ?? null;
|
|
3873
|
-
} catch {
|
|
3874
|
-
return null;
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
function stopWatchtower() {
|
|
3878
|
-
if (!started) return;
|
|
3879
|
-
loadBridge()?.watchtowerStop();
|
|
3880
|
-
started = false;
|
|
3881
|
-
}
|
|
3882
|
-
function subscribeNavigationToWatchtower(navigationRef) {
|
|
3883
|
-
const unsubscribe = createNavigationTracker(navigationRef, {
|
|
3884
|
-
screenNameFilter: (name) => {
|
|
3885
|
-
try {
|
|
3886
|
-
loadBridge()?.watchtowerSetScreen(name);
|
|
3887
|
-
} catch {
|
|
3888
|
-
}
|
|
3889
|
-
return name;
|
|
3890
|
-
}
|
|
3891
|
-
});
|
|
3892
|
-
try {
|
|
3893
|
-
const route = navigationRef.getCurrentRoute?.();
|
|
3894
|
-
if (route?.name) loadBridge()?.watchtowerSetScreen(route.name);
|
|
3895
|
-
} catch {
|
|
3896
|
-
}
|
|
3897
|
-
return unsubscribe;
|
|
3898
|
-
}
|
|
3899
|
-
function WatchtowerProvider(props) {
|
|
3900
|
-
const { config, navigationRef, children } = props;
|
|
3901
|
-
React2__default.default.useEffect(() => {
|
|
3902
|
-
startWatchtower(config);
|
|
3903
|
-
const unsubscribe = subscribeNavigationToWatchtower(navigationRef);
|
|
3904
|
-
return () => {
|
|
3905
|
-
unsubscribe();
|
|
3906
|
-
stopWatchtower();
|
|
3907
|
-
};
|
|
3908
|
-
}, []);
|
|
3909
|
-
return React2__default.default.createElement(React2__default.default.Fragment, null, children);
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
3318
|
// ../watchtower-recorder/dist/index.mjs
|
|
3913
3319
|
var CreateElementNode = (id, parentID, index, tag) => [8, id, parentID, index, tag];
|
|
3914
3320
|
var CreateTextNode = (id, parentID, index) => [
|
|
@@ -3936,6 +3342,12 @@ var RemoveNodeAttribute = (id, name) => [
|
|
|
3936
3342
|
name
|
|
3937
3343
|
];
|
|
3938
3344
|
var SetNodeData = (id, data) => [14, id, data];
|
|
3345
|
+
var SetNodeScroll = (id, x, y) => [
|
|
3346
|
+
16,
|
|
3347
|
+
id,
|
|
3348
|
+
x,
|
|
3349
|
+
y
|
|
3350
|
+
];
|
|
3939
3351
|
var MobileTap = (id, hesitationMs, label, selector, normX, normY) => [68, id, hesitationMs, label, selector, normX, normY];
|
|
3940
3352
|
var ScreenView = (name, prevName) => [
|
|
3941
3353
|
200,
|
|
@@ -3957,677 +3369,1419 @@ var Mirror = class {
|
|
|
3957
3369
|
__publicField(this, "byId", /* @__PURE__ */ new Map());
|
|
3958
3370
|
__publicField(this, "next", 1);
|
|
3959
3371
|
}
|
|
3960
|
-
// 0 is reserved for "no/root parent"
|
|
3961
|
-
/** Existing id for a node, or undefined if never registered. */
|
|
3962
|
-
peek(key) {
|
|
3963
|
-
return this.ids.get(key);
|
|
3372
|
+
// 0 is reserved for "no/root parent"
|
|
3373
|
+
/** Existing id for a node, or undefined if never registered. */
|
|
3374
|
+
peek(key) {
|
|
3375
|
+
return this.ids.get(key);
|
|
3376
|
+
}
|
|
3377
|
+
/** Id for a node, assigning a fresh one on first sight. Second tuple element
|
|
3378
|
+
* is true when the id was just created (→ emit a Create* message). */
|
|
3379
|
+
getId(key) {
|
|
3380
|
+
const existing = this.ids.get(key);
|
|
3381
|
+
if (existing !== void 0) return [existing, false];
|
|
3382
|
+
const id = this.next++;
|
|
3383
|
+
this.ids.set(key, id);
|
|
3384
|
+
this.byId.set(id, key);
|
|
3385
|
+
return [id, true];
|
|
3386
|
+
}
|
|
3387
|
+
getKey(id) {
|
|
3388
|
+
return this.byId.get(id);
|
|
3389
|
+
}
|
|
3390
|
+
/** Drop a node's id (on unmount). Idempotent. */
|
|
3391
|
+
release(key) {
|
|
3392
|
+
const id = this.ids.get(key);
|
|
3393
|
+
if (id === void 0) return void 0;
|
|
3394
|
+
this.ids.delete(key);
|
|
3395
|
+
this.byId.delete(id);
|
|
3396
|
+
return id;
|
|
3397
|
+
}
|
|
3398
|
+
get size() {
|
|
3399
|
+
return this.byId.size;
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
var emptyIndex = () => /* @__PURE__ */ new Map();
|
|
3403
|
+
function indexTree(tree, mirror, prev) {
|
|
3404
|
+
const next = /* @__PURE__ */ new Map();
|
|
3405
|
+
const order = [];
|
|
3406
|
+
walk(tree, (node, parent, idx) => {
|
|
3407
|
+
const [id] = mirror.getId(node.key);
|
|
3408
|
+
const parentId = parent ? mirror.getId(parent.key)[0] : 0;
|
|
3409
|
+
next.set(id, {
|
|
3410
|
+
id,
|
|
3411
|
+
parentId,
|
|
3412
|
+
index: idx,
|
|
3413
|
+
tag: node.tag,
|
|
3414
|
+
attrs: node.attrs,
|
|
3415
|
+
text: node.text
|
|
3416
|
+
});
|
|
3417
|
+
order.push(id);
|
|
3418
|
+
});
|
|
3419
|
+
return { next, order };
|
|
3420
|
+
}
|
|
3421
|
+
function attrDiff(id, prev, next) {
|
|
3422
|
+
const out = [];
|
|
3423
|
+
for (const k of Object.keys(next)) {
|
|
3424
|
+
if (prev[k] !== next[k]) out.push(SetNodeAttribute(id, k, next[k]));
|
|
3425
|
+
}
|
|
3426
|
+
for (const k of Object.keys(prev)) {
|
|
3427
|
+
if (!(k in next)) out.push(RemoveNodeAttribute(id, k));
|
|
3428
|
+
}
|
|
3429
|
+
return out;
|
|
3430
|
+
}
|
|
3431
|
+
function diff(prev, next, mirror) {
|
|
3432
|
+
const { next: nextIndex, order } = indexTree(next, mirror);
|
|
3433
|
+
const messages = [];
|
|
3434
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3435
|
+
for (const id of prev.keys()) if (!nextIndex.has(id)) removed.add(id);
|
|
3436
|
+
for (const id of removed) {
|
|
3437
|
+
const rec = prev.get(id);
|
|
3438
|
+
if (!removed.has(rec.parentId)) messages.push(RemoveNode(id));
|
|
3439
|
+
}
|
|
3440
|
+
for (const id of order) {
|
|
3441
|
+
const rec = nextIndex.get(id);
|
|
3442
|
+
const before = prev.get(id);
|
|
3443
|
+
if (!before) {
|
|
3444
|
+
if (rec.tag === "#text") {
|
|
3445
|
+
messages.push(CreateTextNode(id, rec.parentId, rec.index));
|
|
3446
|
+
} else {
|
|
3447
|
+
messages.push(CreateElementNode(id, rec.parentId, rec.index, rec.tag));
|
|
3448
|
+
}
|
|
3449
|
+
for (const k of Object.keys(rec.attrs)) messages.push(SetNodeAttribute(id, k, rec.attrs[k]));
|
|
3450
|
+
if (rec.text !== void 0 && rec.text !== "") messages.push(SetNodeData(id, rec.text));
|
|
3451
|
+
} else {
|
|
3452
|
+
if (before.parentId !== rec.parentId || before.index !== rec.index) {
|
|
3453
|
+
messages.push(MoveNode(id, rec.parentId, rec.index));
|
|
3454
|
+
}
|
|
3455
|
+
messages.push(...attrDiff(id, before.attrs, rec.attrs));
|
|
3456
|
+
if ((before.text ?? "") !== (rec.text ?? "")) messages.push(SetNodeData(id, rec.text ?? ""));
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return { messages, index: nextIndex };
|
|
3460
|
+
}
|
|
3461
|
+
function stableAnchor(step) {
|
|
3462
|
+
if (step.attrs.testID) return `@${step.attrs.testID}`;
|
|
3463
|
+
if (step.attrs.nativeID) return `#${step.attrs.nativeID}`;
|
|
3464
|
+
return null;
|
|
3465
|
+
}
|
|
3466
|
+
function computeSelector(path) {
|
|
3467
|
+
if (path.length === 0) return "";
|
|
3468
|
+
const self = path[path.length - 1];
|
|
3469
|
+
const own = stableAnchor(self);
|
|
3470
|
+
if (own) return own;
|
|
3471
|
+
const parts = [];
|
|
3472
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
3473
|
+
const step = path[i];
|
|
3474
|
+
const anchor = stableAnchor(step);
|
|
3475
|
+
if (anchor && i !== path.length - 1) {
|
|
3476
|
+
parts.unshift(anchor);
|
|
3477
|
+
return parts.join(" > ");
|
|
3478
|
+
}
|
|
3479
|
+
const a11y = step.attrs.accessibilityLabel;
|
|
3480
|
+
const comp = step.attrs.rnComponent;
|
|
3481
|
+
if (i === path.length - 1 && a11y) {
|
|
3482
|
+
parts.unshift(`${step.tag}[a11y="${truncate(a11y)}"]`);
|
|
3483
|
+
} else if (comp && comp !== step.tag) {
|
|
3484
|
+
parts.unshift(`${comp}`);
|
|
3485
|
+
} else {
|
|
3486
|
+
parts.unshift(`${step.tag}:nth-of-type(${step.nthOfType})`);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
return parts.join(" > ");
|
|
3490
|
+
}
|
|
3491
|
+
function truncate(s, n = 40) {
|
|
3492
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
3493
|
+
}
|
|
3494
|
+
function computeLabel(attrs, text) {
|
|
3495
|
+
if (attrs.accessibilityLabel) return truncate(attrs.accessibilityLabel, 100);
|
|
3496
|
+
if (attrs.testID) return attrs.testID;
|
|
3497
|
+
if (text && text.trim()) return truncate(text.trim(), 100);
|
|
3498
|
+
if (attrs.rnComponent) return attrs.rnComponent;
|
|
3499
|
+
return "";
|
|
3500
|
+
}
|
|
3501
|
+
var RAGE_WINDOW_MS = 800;
|
|
3502
|
+
var RAGE_MIN_COUNT = 3;
|
|
3503
|
+
var OUTCOME_WINDOW_MS = 1e3;
|
|
3504
|
+
var TOGGLE_ATTR_MAX = 4;
|
|
3505
|
+
var OUTCOME_LABELS = {
|
|
3506
|
+
navigated: "Navigated to a new screen",
|
|
3507
|
+
opened_overlay: "Opened a modal / overlay",
|
|
3508
|
+
closed_overlay: "Dismissed a modal / overlay",
|
|
3509
|
+
toggled: "Toggled UI state",
|
|
3510
|
+
content_changed: "Updated on-screen content",
|
|
3511
|
+
no_effect: "Nothing changed (dead tap)"
|
|
3512
|
+
};
|
|
3513
|
+
var InteractionAnalyzer = class {
|
|
3514
|
+
constructor() {
|
|
3515
|
+
__publicField(this, "activity", []);
|
|
3516
|
+
__publicField(this, "screenViews", []);
|
|
3517
|
+
__publicField(this, "taps", []);
|
|
3518
|
+
}
|
|
3519
|
+
recordActivity(a) {
|
|
3520
|
+
this.activity.push(a);
|
|
3521
|
+
}
|
|
3522
|
+
recordScreenView(t) {
|
|
3523
|
+
this.screenViews.push(t);
|
|
3524
|
+
}
|
|
3525
|
+
/** Register a tap; returns whether it is (so far) a rage tap and the count. */
|
|
3526
|
+
recordTap(tap) {
|
|
3527
|
+
this.taps.push(tap);
|
|
3528
|
+
const from = tap.t - RAGE_WINDOW_MS;
|
|
3529
|
+
const sameSel = this.taps.filter((x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t);
|
|
3530
|
+
return { rage: sameSel.length >= RAGE_MIN_COUNT, count: sameSel.length };
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Evaluate a tap once its outcome window has closed. Classifies the post-tap
|
|
3534
|
+
* mutation diff into a semantic outcome and flags dead/rage.
|
|
3535
|
+
*/
|
|
3536
|
+
evaluate(tap) {
|
|
3537
|
+
const end = tap.t + OUTCOME_WINDOW_MS;
|
|
3538
|
+
const win = this.activity.filter((a) => a.t > tap.t && a.t <= end);
|
|
3539
|
+
const navigated = this.screenViews.some((t) => t > tap.t && t <= end);
|
|
3540
|
+
const sum = (f) => win.reduce((n, a) => n + f(a), 0);
|
|
3541
|
+
const structural = sum((a) => a.create) + sum((a) => a.remove) + sum((a) => a.move);
|
|
3542
|
+
const attrs = sum((a) => a.attr) + sum((a) => a.text);
|
|
3543
|
+
const mutationCount = structural + attrs;
|
|
3544
|
+
const overlayAdd = win.some((a) => a.overlayAdd);
|
|
3545
|
+
const overlayRemove = win.some((a) => a.overlayRemove);
|
|
3546
|
+
let kind;
|
|
3547
|
+
if (navigated) kind = "navigated";
|
|
3548
|
+
else if (overlayAdd) kind = "opened_overlay";
|
|
3549
|
+
else if (overlayRemove) kind = "closed_overlay";
|
|
3550
|
+
else if (mutationCount === 0) kind = "no_effect";
|
|
3551
|
+
else if (structural === 0 && attrs <= TOGGLE_ATTR_MAX) kind = "toggled";
|
|
3552
|
+
else kind = "content_changed";
|
|
3553
|
+
const from = tap.t - RAGE_WINDOW_MS;
|
|
3554
|
+
const rageCount = this.taps.filter(
|
|
3555
|
+
(x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t
|
|
3556
|
+
).length;
|
|
3557
|
+
let frustration = null;
|
|
3558
|
+
if (kind === "no_effect") frustration = { kind: "dead", count: 1 };
|
|
3559
|
+
if (rageCount >= RAGE_MIN_COUNT) frustration = { kind: "rage", count: rageCount };
|
|
3560
|
+
return { frustration, outcome: { kind, label: OUTCOME_LABELS[kind], mutationCount } };
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
var OVERLAY_TAGS = /* @__PURE__ */ new Set(["Modal"]);
|
|
3564
|
+
function isOverlayRecord(tag, attrs) {
|
|
3565
|
+
if (tag && OVERLAY_TAGS.has(tag)) return true;
|
|
3566
|
+
if (attrs && (attrs.wtOverlay === "true" || attrs.role === "dialog")) return true;
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
var Recorder = class {
|
|
3570
|
+
constructor() {
|
|
3571
|
+
__publicField(this, "mirror", new Mirror());
|
|
3572
|
+
__publicField(this, "index", emptyIndex());
|
|
3573
|
+
__publicField(this, "analyzer", new InteractionAnalyzer());
|
|
3574
|
+
__publicField(this, "stream", []);
|
|
3575
|
+
__publicField(this, "pending", []);
|
|
3576
|
+
__publicField(this, "viewport", { w: 0, h: 0 });
|
|
3577
|
+
}
|
|
3578
|
+
emit(t, m) {
|
|
3579
|
+
this.stream.push({ t, m });
|
|
3580
|
+
}
|
|
3581
|
+
setViewport(w, h, t) {
|
|
3582
|
+
this.viewport = { w, h };
|
|
3583
|
+
this.emit(t, [5, w, h]);
|
|
3584
|
+
}
|
|
3585
|
+
/** Feed a fresh tree snapshot; diffs against the last and streams mutations. */
|
|
3586
|
+
commit(tree, t) {
|
|
3587
|
+
const prev = this.index;
|
|
3588
|
+
const { messages, index } = diff(prev, tree, this.mirror);
|
|
3589
|
+
for (const m of messages) this.emit(t, m);
|
|
3590
|
+
const newIds = /* @__PURE__ */ new Set();
|
|
3591
|
+
for (const m of messages) {
|
|
3592
|
+
if (m[0] === 8 || m[0] === 9) newIds.add(m[1]);
|
|
3593
|
+
}
|
|
3594
|
+
let create = 0, remove = 0, move = 0, attr = 0, text = 0;
|
|
3595
|
+
let overlayAdd = false, overlayRemove = false;
|
|
3596
|
+
for (const m of messages) {
|
|
3597
|
+
switch (m[0]) {
|
|
3598
|
+
case 8:
|
|
3599
|
+
create++;
|
|
3600
|
+
if (isOverlayRecord(index.get(m[1])?.tag, index.get(m[1])?.attrs)) overlayAdd = true;
|
|
3601
|
+
break;
|
|
3602
|
+
case 9:
|
|
3603
|
+
create++;
|
|
3604
|
+
break;
|
|
3605
|
+
case 11:
|
|
3606
|
+
remove++;
|
|
3607
|
+
if (isOverlayRecord(prev.get(m[1])?.tag, prev.get(m[1])?.attrs)) overlayRemove = true;
|
|
3608
|
+
break;
|
|
3609
|
+
case 10:
|
|
3610
|
+
move++;
|
|
3611
|
+
break;
|
|
3612
|
+
case 12:
|
|
3613
|
+
case 13:
|
|
3614
|
+
if (!newIds.has(m[1])) attr++;
|
|
3615
|
+
break;
|
|
3616
|
+
case 14:
|
|
3617
|
+
if (!newIds.has(m[1])) text++;
|
|
3618
|
+
break;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const activity = { t, create, remove, move, attr, text, overlayAdd, overlayRemove };
|
|
3622
|
+
this.analyzer.recordActivity(activity);
|
|
3623
|
+
this.index = index;
|
|
3624
|
+
this.drainPending(t);
|
|
3625
|
+
}
|
|
3626
|
+
tap(input, t) {
|
|
3627
|
+
const [id] = this.mirror.getId(input.key);
|
|
3628
|
+
const selector = computeSelector(input.path);
|
|
3629
|
+
const label = computeLabel(input.attrs, input.text);
|
|
3630
|
+
const nx = Math.round(input.normX * 1e4);
|
|
3631
|
+
const ny = Math.round(input.normY * 1e4);
|
|
3632
|
+
this.emit(t, MobileTap(id, input.hesitationMs ?? 0, label, selector, nx, ny));
|
|
3633
|
+
this.analyzer.recordTap({ t, nodeId: id, selector });
|
|
3634
|
+
this.pending.push({ t, nodeId: id, selector });
|
|
3635
|
+
this.drainPending(t);
|
|
3964
3636
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
if (existing !== void 0) return [existing, false];
|
|
3970
|
-
const id = this.next++;
|
|
3971
|
-
this.ids.set(key, id);
|
|
3972
|
-
this.byId.set(id, key);
|
|
3973
|
-
return [id, true];
|
|
3637
|
+
screenView(name, prevName, t) {
|
|
3638
|
+
this.emit(t, ScreenView(name, prevName));
|
|
3639
|
+
this.analyzer.recordScreenView(t);
|
|
3640
|
+
this.drainPending(t);
|
|
3974
3641
|
}
|
|
3975
|
-
|
|
3976
|
-
|
|
3642
|
+
/** Record a scroll offset for a node (absolute content offset, px). The player
|
|
3643
|
+
* translates the node's children by (-x,-y), reproducing the scroll/drag —
|
|
3644
|
+
* the RN fiber walk can't see native-driven scroll, so it's fed from gestures. */
|
|
3645
|
+
scroll(key, x, y, t) {
|
|
3646
|
+
const [id] = this.mirror.getId(key);
|
|
3647
|
+
this.emit(t, SetNodeScroll(id, Math.round(x), Math.round(y)));
|
|
3977
3648
|
}
|
|
3978
|
-
/**
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
if (id === void 0) return void 0;
|
|
3982
|
-
this.ids.delete(key);
|
|
3983
|
-
this.byId.delete(id);
|
|
3984
|
-
return id;
|
|
3649
|
+
/** Record a node scroll/offset change (from a drag/scroll gesture). */
|
|
3650
|
+
setNodeScroll(nodeId, scrollX, scrollY, t) {
|
|
3651
|
+
this.emit(t, [16, nodeId, scrollX, scrollY]);
|
|
3985
3652
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3653
|
+
/** Evaluate any pending taps whose outcome window has closed by time `t`. */
|
|
3654
|
+
drainPending(t) {
|
|
3655
|
+
const ready = this.pending.filter((p) => t >= p.t + OUTCOME_WINDOW_MS);
|
|
3656
|
+
if (!ready.length) return;
|
|
3657
|
+
this.pending = this.pending.filter((p) => t < p.t + OUTCOME_WINDOW_MS);
|
|
3658
|
+
for (const p of ready) this.evaluateTap(p);
|
|
3659
|
+
}
|
|
3660
|
+
evaluateTap(p) {
|
|
3661
|
+
const at = p.t + OUTCOME_WINDOW_MS;
|
|
3662
|
+
const v = this.analyzer.evaluate(p);
|
|
3663
|
+
if (v.frustration) {
|
|
3664
|
+
this.emit(at, Frustration(v.frustration.kind, p.nodeId, p.selector, v.frustration.count));
|
|
3665
|
+
}
|
|
3666
|
+
this.emit(at, TapOutcome(p.nodeId, v.outcome.kind, v.outcome.label, v.outcome.mutationCount));
|
|
3667
|
+
}
|
|
3668
|
+
/** Explicit clock advance (adapter ticker) to close outcome windows even when
|
|
3669
|
+
* no commit/tap arrives. */
|
|
3670
|
+
advance(t) {
|
|
3671
|
+
this.drainPending(t);
|
|
3672
|
+
}
|
|
3673
|
+
/** Flush all remaining pending taps (session end). `_t` is the session-end
|
|
3674
|
+
* clock, accepted for API symmetry with advance(); evaluation uses each tap's
|
|
3675
|
+
* own outcome window. */
|
|
3676
|
+
finalize(_t) {
|
|
3677
|
+
const ready = this.pending;
|
|
3678
|
+
this.pending = [];
|
|
3679
|
+
for (const p of ready) this.evaluateTap(p);
|
|
3680
|
+
this.stream.sort((a, b) => a.t - b.t);
|
|
3681
|
+
}
|
|
3682
|
+
getStream() {
|
|
3683
|
+
return [...this.stream].sort((a, b) => a.t - b.t);
|
|
3684
|
+
}
|
|
3685
|
+
/** Total messages emitted so far (insertion order). Adapters use this as the
|
|
3686
|
+
* cursor for incremental delta flushing — see `sliceStream`. */
|
|
3687
|
+
get emittedCount() {
|
|
3688
|
+
return this.stream.length;
|
|
3689
|
+
}
|
|
3690
|
+
/** Messages emitted since insertion index `from`, in EMISSION order (not
|
|
3691
|
+
* sorted by time). This is the delta an adapter ships each flush; the cursor
|
|
3692
|
+
* is the count it last confirmed stored. Late-drained outcome messages (whose
|
|
3693
|
+
* `t` is earlier than emission time) appear here at their emission position,
|
|
3694
|
+
* so the server appends verbatim and the reader sorts by `t`. */
|
|
3695
|
+
sliceStream(from) {
|
|
3696
|
+
return this.stream.slice(Math.max(0, from));
|
|
3697
|
+
}
|
|
3698
|
+
get viewportSize() {
|
|
3699
|
+
return this.viewport;
|
|
3988
3700
|
}
|
|
3989
3701
|
};
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
index: idx,
|
|
4001
|
-
tag: node.tag,
|
|
4002
|
-
attrs: node.attrs,
|
|
4003
|
-
text: node.text
|
|
4004
|
-
});
|
|
4005
|
-
order.push(id);
|
|
4006
|
-
});
|
|
4007
|
-
return { next, order };
|
|
3702
|
+
|
|
3703
|
+
// src/react-native/structural/install-hook.ts
|
|
3704
|
+
var g = globalThis;
|
|
3705
|
+
var hookState = g.__SITEPONG_STRUCTURAL_HOOK_STATE__ ?? (g.__SITEPONG_STRUCTURAL_HOOK_STATE__ = {
|
|
3706
|
+
latestRoot: null,
|
|
3707
|
+
dirty: false
|
|
3708
|
+
});
|
|
3709
|
+
function record(root) {
|
|
3710
|
+
hookState.latestRoot = root;
|
|
3711
|
+
hookState.dirty = true;
|
|
4008
3712
|
}
|
|
4009
|
-
function
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
3713
|
+
function installStructuralHook() {
|
|
3714
|
+
if (g.__SITEPONG_STRUCTURAL_HOOK_INSTALLED__) return;
|
|
3715
|
+
const existing = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
3716
|
+
if (existing) {
|
|
3717
|
+
const prevRaw = existing.onCommitFiberRoot;
|
|
3718
|
+
const prev = typeof prevRaw === "function" ? prevRaw.bind(existing) : null;
|
|
3719
|
+
existing.onCommitFiberRoot = (id, root, ...rest) => {
|
|
3720
|
+
record(root);
|
|
3721
|
+
return prev ? prev(id, root, ...rest) : void 0;
|
|
3722
|
+
};
|
|
3723
|
+
} else {
|
|
3724
|
+
const renderers = /* @__PURE__ */ new Map();
|
|
3725
|
+
g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
3726
|
+
renderers,
|
|
3727
|
+
supportsFiber: true,
|
|
3728
|
+
inject(renderer) {
|
|
3729
|
+
const id = renderers.size + 1;
|
|
3730
|
+
renderers.set(id, renderer);
|
|
3731
|
+
return id;
|
|
3732
|
+
},
|
|
3733
|
+
onCommitFiberRoot(_id, root) {
|
|
3734
|
+
record(root);
|
|
3735
|
+
},
|
|
3736
|
+
onCommitFiberUnmount() {
|
|
3737
|
+
},
|
|
3738
|
+
onScheduleFiberRoot() {
|
|
3739
|
+
},
|
|
3740
|
+
on() {
|
|
3741
|
+
},
|
|
3742
|
+
off() {
|
|
3743
|
+
},
|
|
3744
|
+
sub() {
|
|
3745
|
+
return () => {
|
|
3746
|
+
};
|
|
3747
|
+
},
|
|
3748
|
+
checkDCE() {
|
|
3749
|
+
}
|
|
3750
|
+
};
|
|
4013
3751
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
3752
|
+
g.__SITEPONG_STRUCTURAL_HOOK_INSTALLED__ = true;
|
|
3753
|
+
}
|
|
3754
|
+
var HostComponent = 5;
|
|
3755
|
+
var HostText = 6;
|
|
3756
|
+
function mapTag(type) {
|
|
3757
|
+
if (typeof type !== "string") return "View";
|
|
3758
|
+
const t = type;
|
|
3759
|
+
if (/TextInput|TextField/i.test(t)) return "TextInput";
|
|
3760
|
+
if (/Image/i.test(t)) return "Image";
|
|
3761
|
+
if (/ScrollView/i.test(t)) return "ScrollView";
|
|
3762
|
+
if (/Modal/i.test(t)) return "Modal";
|
|
3763
|
+
if (/Text|RawText/i.test(t)) return "Text";
|
|
3764
|
+
if (/Pressable|Button|Touchable/i.test(t)) return "Pressable";
|
|
3765
|
+
return "View";
|
|
3766
|
+
}
|
|
3767
|
+
function nativeTagOf(stateNode) {
|
|
3768
|
+
if (!stateNode) return void 0;
|
|
3769
|
+
return stateNode.__nativeTag ?? stateNode._nativeTag ?? stateNode.canonical?.nativeTag ?? stateNode.canonical?._nativeTag ?? (typeof stateNode === "number" ? stateNode : void 0);
|
|
3770
|
+
}
|
|
3771
|
+
var STYLE_KEYS = /* @__PURE__ */ new Set([
|
|
3772
|
+
"flex",
|
|
3773
|
+
"flexDirection",
|
|
3774
|
+
"flexGrow",
|
|
3775
|
+
"flexShrink",
|
|
3776
|
+
"flexWrap",
|
|
3777
|
+
"alignItems",
|
|
3778
|
+
"alignSelf",
|
|
3779
|
+
"justifyContent",
|
|
3780
|
+
"width",
|
|
3781
|
+
"height",
|
|
3782
|
+
"minWidth",
|
|
3783
|
+
"minHeight",
|
|
3784
|
+
"maxWidth",
|
|
3785
|
+
"maxHeight",
|
|
3786
|
+
"margin",
|
|
3787
|
+
"marginTop",
|
|
3788
|
+
"marginBottom",
|
|
3789
|
+
"marginLeft",
|
|
3790
|
+
"marginRight",
|
|
3791
|
+
"marginHorizontal",
|
|
3792
|
+
"marginVertical",
|
|
3793
|
+
"padding",
|
|
3794
|
+
"paddingTop",
|
|
3795
|
+
"paddingBottom",
|
|
3796
|
+
"paddingLeft",
|
|
3797
|
+
"paddingRight",
|
|
3798
|
+
"paddingHorizontal",
|
|
3799
|
+
"paddingVertical",
|
|
3800
|
+
"backgroundColor",
|
|
3801
|
+
"borderRadius",
|
|
3802
|
+
"borderWidth",
|
|
3803
|
+
"borderColor",
|
|
3804
|
+
"opacity",
|
|
3805
|
+
"position",
|
|
3806
|
+
"top",
|
|
3807
|
+
"left",
|
|
3808
|
+
"right",
|
|
3809
|
+
"bottom",
|
|
3810
|
+
"color",
|
|
3811
|
+
"fontSize",
|
|
3812
|
+
"fontWeight",
|
|
3813
|
+
"textAlign",
|
|
3814
|
+
"lineHeight",
|
|
3815
|
+
"gap",
|
|
3816
|
+
"overflow",
|
|
3817
|
+
"shadowColor",
|
|
3818
|
+
"shadowOpacity",
|
|
3819
|
+
"shadowRadius",
|
|
3820
|
+
"elevation",
|
|
3821
|
+
"transform",
|
|
3822
|
+
"zIndex",
|
|
3823
|
+
"aspectRatio"
|
|
3824
|
+
]);
|
|
3825
|
+
function extractStyle(style) {
|
|
3826
|
+
try {
|
|
3827
|
+
const flat = reactNative.StyleSheet.flatten(style);
|
|
3828
|
+
if (!flat) return void 0;
|
|
3829
|
+
const out = {};
|
|
3830
|
+
for (const k of Object.keys(flat)) if (STYLE_KEYS.has(k)) out[k] = flat[k];
|
|
3831
|
+
return Object.keys(out).length ? JSON.stringify(out) : void 0;
|
|
3832
|
+
} catch {
|
|
3833
|
+
return void 0;
|
|
4016
3834
|
}
|
|
4017
|
-
return out;
|
|
4018
3835
|
}
|
|
4019
|
-
function
|
|
4020
|
-
const
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
3836
|
+
function extractAttrs(tag, props, componentName2) {
|
|
3837
|
+
const attrs = {};
|
|
3838
|
+
if (!props) return attrs;
|
|
3839
|
+
if (props.testID) attrs.testID = String(props.testID);
|
|
3840
|
+
if (props.nativeID) attrs.nativeID = String(props.nativeID);
|
|
3841
|
+
if (props.accessibilityLabel) attrs.accessibilityLabel = String(props.accessibilityLabel);
|
|
3842
|
+
if (props.accessibilityRole) attrs.role = String(props.accessibilityRole);
|
|
3843
|
+
if (props["aria-label"]) attrs.accessibilityLabel = String(props["aria-label"]);
|
|
3844
|
+
const style = extractStyle(props.style);
|
|
3845
|
+
if (style) attrs.style = style;
|
|
3846
|
+
if (tag === "Image") {
|
|
3847
|
+
const uri = props.source?.uri ?? (typeof props.source === "string" ? props.source : void 0);
|
|
3848
|
+
if (uri) attrs.src = String(uri);
|
|
4027
3849
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
if (
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
if ((before.text ?? "") !== (rec.text ?? "")) messages.push(SetNodeData(id, rec.text ?? ""));
|
|
3850
|
+
if (componentName2) attrs.rnComponent = componentName2;
|
|
3851
|
+
if (props.wtOverlay || props.accessibilityViewIsModal) attrs.wtOverlay = "true";
|
|
3852
|
+
if (props.wtSensitive || props.secureTextEntry) attrs.wtSensitive = "true";
|
|
3853
|
+
return attrs;
|
|
3854
|
+
}
|
|
3855
|
+
function collectText(fiber) {
|
|
3856
|
+
let out = "";
|
|
3857
|
+
const visit = (f) => {
|
|
3858
|
+
if (!f) return;
|
|
3859
|
+
if (f.tag === HostText) {
|
|
3860
|
+
const p = f.memoizedProps;
|
|
3861
|
+
if (typeof p === "string") out += p;
|
|
3862
|
+
else if (p && typeof p.text === "string") out += p.text;
|
|
3863
|
+
} else if (f.tag === HostComponent && typeof f.type === "string" && /RawText|VirtualText/i.test(f.type)) {
|
|
3864
|
+
const p = f.memoizedProps;
|
|
3865
|
+
if (p && typeof p.text === "string") out += p.text;
|
|
4045
3866
|
}
|
|
3867
|
+
visit(f.child);
|
|
3868
|
+
visit(f.sibling);
|
|
3869
|
+
};
|
|
3870
|
+
visit(fiber.child);
|
|
3871
|
+
return out;
|
|
3872
|
+
}
|
|
3873
|
+
function buildHostChildren(fiber, inheritedName, index, path, scrollTag) {
|
|
3874
|
+
const out = [];
|
|
3875
|
+
let f = fiber;
|
|
3876
|
+
while (f) {
|
|
3877
|
+
collectFrom(f, inheritedName, out, index, path, scrollTag);
|
|
3878
|
+
f = f.sibling;
|
|
4046
3879
|
}
|
|
4047
|
-
return
|
|
3880
|
+
return out;
|
|
4048
3881
|
}
|
|
4049
|
-
function
|
|
4050
|
-
|
|
4051
|
-
if (
|
|
4052
|
-
return
|
|
3882
|
+
function componentName(fiber) {
|
|
3883
|
+
const t = fiber?.type;
|
|
3884
|
+
if (typeof t === "function") return t.displayName || t.name || void 0;
|
|
3885
|
+
if (t && typeof t === "object") return t.displayName || void 0;
|
|
3886
|
+
return void 0;
|
|
4053
3887
|
}
|
|
4054
|
-
function
|
|
4055
|
-
if (
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
const
|
|
4062
|
-
const
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
3888
|
+
function collectFrom(fiber, inheritedName, out, index, parentPath, scrollTag) {
|
|
3889
|
+
if (!fiber) return;
|
|
3890
|
+
if (fiber.tag === HostComponent) {
|
|
3891
|
+
const tag = mapTag(fiber.type);
|
|
3892
|
+
const attrs = extractAttrs(tag, fiber.memoizedProps, inheritedName);
|
|
3893
|
+
const stateNode = fiber.stateNode;
|
|
3894
|
+
const key = stateNode && typeof stateNode === "object" ? stateNode : fiber;
|
|
3895
|
+
const isTextLeaf = tag === "Text";
|
|
3896
|
+
const text = isTextLeaf ? collectText(fiber) : void 0;
|
|
3897
|
+
const nthOfType = out.filter((n) => n.tag === tag).length + 1;
|
|
3898
|
+
const myPath = [...parentPath, { tag, attrs, nthOfType }];
|
|
3899
|
+
const nt = nativeTagOf(stateNode);
|
|
3900
|
+
const childScrollTag = tag === "ScrollView" && nt !== void 0 ? nt : scrollTag;
|
|
3901
|
+
const children = isTextLeaf ? [] : buildHostChildren(fiber.child, void 0, index, myPath, childScrollTag);
|
|
3902
|
+
const vnode = { key, tag, attrs, children, ...text ? { text } : {} };
|
|
3903
|
+
out.push(vnode);
|
|
3904
|
+
if (nt !== void 0) index.set(nt, { node: vnode, path: myPath, scrollAncestorTag: scrollTag });
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
const name = componentName(fiber) ?? inheritedName;
|
|
3908
|
+
let child = fiber.child;
|
|
3909
|
+
while (child) {
|
|
3910
|
+
collectFrom(child, name, out, index, parentPath, scrollTag);
|
|
3911
|
+
child = child.sibling;
|
|
4076
3912
|
}
|
|
4077
|
-
return parts.join(" > ");
|
|
4078
3913
|
}
|
|
4079
|
-
function
|
|
4080
|
-
|
|
3914
|
+
function walkFiberRoot(root) {
|
|
3915
|
+
const index = /* @__PURE__ */ new Map();
|
|
3916
|
+
const current = root?.current;
|
|
3917
|
+
if (!current) return { tree: null, index };
|
|
3918
|
+
const rootKey = root ?? current;
|
|
3919
|
+
const children = buildHostChildren(current.child, void 0, index, [], void 0);
|
|
3920
|
+
const tree = { key: rootKey, tag: "View", attrs: { style: JSON.stringify({ flex: 1 }) }, children };
|
|
3921
|
+
return { tree, index };
|
|
4081
3922
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
3923
|
+
|
|
3924
|
+
// src/react-native/structural/capture.ts
|
|
3925
|
+
var WALK_BUDGET_MS = 24;
|
|
3926
|
+
var MAX_OVERLOAD_SKIP = 8;
|
|
3927
|
+
function genSessionId() {
|
|
3928
|
+
const rand = Math.floor(Math.random() * 16777215).toString(36);
|
|
3929
|
+
return `wt-${Date.now().toString(36)}-${rand}`;
|
|
4088
3930
|
}
|
|
4089
|
-
var
|
|
4090
|
-
var RAGE_MIN_COUNT = 3;
|
|
4091
|
-
var OUTCOME_WINDOW_MS = 1e3;
|
|
4092
|
-
var TOGGLE_ATTR_MAX = 4;
|
|
4093
|
-
var OUTCOME_LABELS = {
|
|
4094
|
-
navigated: "Navigated to a new screen",
|
|
4095
|
-
opened_overlay: "Opened a modal / overlay",
|
|
4096
|
-
closed_overlay: "Dismissed a modal / overlay",
|
|
4097
|
-
toggled: "Toggled UI state",
|
|
4098
|
-
content_changed: "Updated on-screen content",
|
|
4099
|
-
no_effect: "Nothing changed (dead tap)"
|
|
4100
|
-
};
|
|
4101
|
-
var InteractionAnalyzer = class {
|
|
3931
|
+
var StructuralCapture = class {
|
|
4102
3932
|
constructor() {
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
3933
|
+
this.recorder = new Recorder();
|
|
3934
|
+
this.currentIndex = /* @__PURE__ */ new Map();
|
|
3935
|
+
this.started = 0;
|
|
3936
|
+
this.sampleTimer = null;
|
|
3937
|
+
this.flushTimer = null;
|
|
3938
|
+
this.screen = "";
|
|
3939
|
+
this.prevScreen = "";
|
|
3940
|
+
this.cfg = null;
|
|
3941
|
+
this.sessionId = "";
|
|
3942
|
+
this.running = false;
|
|
3943
|
+
// Incremental delivery: cursor into the recorder's emission stream that has
|
|
3944
|
+
// been CONFIRMED stored. Everything at index >= sentCount is unsent (the
|
|
3945
|
+
// offline buffer). Advances only on a 2xx.
|
|
3946
|
+
this.sentCount = 0;
|
|
3947
|
+
this.flushing = false;
|
|
3948
|
+
// Persistent per-ScrollView content offset (keyed by native tag), so scroll
|
|
3949
|
+
// continues from where it was rather than resetting each gesture.
|
|
3950
|
+
this.scrollOffsets = /* @__PURE__ */ new Map();
|
|
3951
|
+
// Adaptive sampling: skip this many upcoming ticks after a heavy walk.
|
|
3952
|
+
this.overloadSkip = 0;
|
|
3953
|
+
this.appStateSub = null;
|
|
3954
|
+
this.teardowns = [];
|
|
4106
3955
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
3956
|
+
now() {
|
|
3957
|
+
return Date.now() - this.started;
|
|
4109
3958
|
}
|
|
4110
|
-
|
|
4111
|
-
this.
|
|
3959
|
+
isRunning() {
|
|
3960
|
+
return this.running;
|
|
4112
3961
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
this.taps.push(tap);
|
|
4116
|
-
const from = tap.t - RAGE_WINDOW_MS;
|
|
4117
|
-
const sameSel = this.taps.filter((x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t);
|
|
4118
|
-
return { rage: sameSel.length >= RAGE_MIN_COUNT, count: sameSel.length };
|
|
3962
|
+
getSessionId() {
|
|
3963
|
+
return this.sessionId;
|
|
4119
3964
|
}
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
else if (mutationCount === 0) kind = "no_effect";
|
|
4139
|
-
else if (structural === 0 && attrs <= TOGGLE_ATTR_MAX) kind = "toggled";
|
|
4140
|
-
else kind = "content_changed";
|
|
4141
|
-
const from = tap.t - RAGE_WINDOW_MS;
|
|
4142
|
-
const rageCount = this.taps.filter(
|
|
4143
|
-
(x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t
|
|
4144
|
-
).length;
|
|
4145
|
-
let frustration = null;
|
|
4146
|
-
if (kind === "no_effect") frustration = { kind: "dead", count: 1 };
|
|
4147
|
-
if (rageCount >= RAGE_MIN_COUNT) frustration = { kind: "rage", count: rageCount };
|
|
4148
|
-
return { frustration, outcome: { kind, label: OUTCOME_LABELS[kind], mutationCount } };
|
|
3965
|
+
start(cfg) {
|
|
3966
|
+
if (this.running) return;
|
|
3967
|
+
installStructuralHook();
|
|
3968
|
+
this.cfg = cfg;
|
|
3969
|
+
this.sessionId = cfg.sessionId || genSessionId();
|
|
3970
|
+
this.started = Date.now();
|
|
3971
|
+
this.running = true;
|
|
3972
|
+
this.sentCount = 0;
|
|
3973
|
+
this.scrollOffsets.clear();
|
|
3974
|
+
this.overloadSkip = 0;
|
|
3975
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
3976
|
+
this.recorder.setViewport(Math.round(width), Math.round(height), 0);
|
|
3977
|
+
this.sampleTimer = setInterval(() => this.sample(), cfg.sampleIntervalMs ?? 120);
|
|
3978
|
+
this.flushTimer = setInterval(() => void this.flush(), cfg.flushIntervalMs ?? 4e3);
|
|
3979
|
+
this.installCrashFlush();
|
|
3980
|
+
this.installBackgroundFlush();
|
|
3981
|
+
if (cfg.debug) console.log("[structural] started", this.sessionId);
|
|
3982
|
+
setTimeout(() => this.sample(true), 0);
|
|
4149
3983
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
3984
|
+
/** Best-effort flush the pre-crash tail before the app tears down. */
|
|
3985
|
+
installCrashFlush() {
|
|
3986
|
+
if (typeof ErrorUtils === "undefined" || typeof ErrorUtils.setGlobalHandler !== "function") return;
|
|
3987
|
+
const prev = ErrorUtils.getGlobalHandler?.();
|
|
3988
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
3989
|
+
void this.flush();
|
|
3990
|
+
if (typeof prev === "function") prev(error, isFatal);
|
|
3991
|
+
});
|
|
3992
|
+
this.teardowns.push(() => {
|
|
3993
|
+
if (typeof prev === "function") ErrorUtils.setGlobalHandler(prev);
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
/** Flush on background/inactive so timer suspension doesn't strand the tail. */
|
|
3997
|
+
installBackgroundFlush() {
|
|
3998
|
+
const onChange = (state) => {
|
|
3999
|
+
if (state === "background" || state === "inactive") void this.flush();
|
|
4000
|
+
};
|
|
4001
|
+
this.appStateSub = reactNative.AppState.addEventListener("change", onChange);
|
|
4002
|
+
this.teardowns.push(() => {
|
|
4003
|
+
this.appStateSub?.remove();
|
|
4004
|
+
this.appStateSub = null;
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
sample(force = false) {
|
|
4008
|
+
if (!this.running) return;
|
|
4009
|
+
if (!force && this.overloadSkip > 0) {
|
|
4010
|
+
this.overloadSkip--;
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
if (!force && !hookState.dirty) return;
|
|
4014
|
+
hookState.dirty = false;
|
|
4015
|
+
const root = hookState.latestRoot;
|
|
4016
|
+
if (!root) return;
|
|
4017
|
+
const t0 = Date.now();
|
|
4018
|
+
try {
|
|
4019
|
+
const { tree, index } = walkFiberRoot(root);
|
|
4020
|
+
this.currentIndex = index;
|
|
4021
|
+
this.recorder.commit(tree, this.now());
|
|
4022
|
+
} catch (e) {
|
|
4023
|
+
if (this.cfg?.debug) console.log("[structural] sample error", String(e));
|
|
4024
|
+
}
|
|
4025
|
+
const cost = Date.now() - t0;
|
|
4026
|
+
if (cost > WALK_BUDGET_MS) {
|
|
4027
|
+
this.overloadSkip = Math.min(MAX_OVERLOAD_SKIP, Math.ceil(cost / WALK_BUDGET_MS));
|
|
4028
|
+
}
|
|
4165
4029
|
}
|
|
4166
|
-
|
|
4167
|
-
this.
|
|
4030
|
+
setScreen(name) {
|
|
4031
|
+
if (!this.running || name === this.screen) return;
|
|
4032
|
+
this.prevScreen = this.screen;
|
|
4033
|
+
this.screen = name;
|
|
4034
|
+
this.sample(true);
|
|
4035
|
+
this.recorder.screenView(name, this.prevScreen, this.now());
|
|
4168
4036
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4037
|
+
/** Resolve a native touch to a node + emit a tap. `target` is the RN
|
|
4038
|
+
* nativeTag from `nativeEvent.target`. */
|
|
4039
|
+
handleTap(target, pageX, pageY) {
|
|
4040
|
+
if (!this.running) return;
|
|
4041
|
+
const entry = this.currentIndex.get(target);
|
|
4042
|
+
if (!entry) return;
|
|
4043
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
4044
|
+
this.recorder.tap(
|
|
4045
|
+
{
|
|
4046
|
+
key: entry.node.key,
|
|
4047
|
+
path: entry.path,
|
|
4048
|
+
attrs: entry.node.attrs,
|
|
4049
|
+
text: entry.node.text,
|
|
4050
|
+
normX: pageX / (width || 1),
|
|
4051
|
+
normY: pageY / (height || 1)
|
|
4052
|
+
},
|
|
4053
|
+
this.now()
|
|
4054
|
+
);
|
|
4172
4055
|
}
|
|
4173
|
-
/** Feed a
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4056
|
+
/** Feed a scroll/drag delta from a touch onto the nearest scrollable ancestor
|
|
4057
|
+
* of the touched node. `target` is the RN nativeTag under the finger; `dx`/`dy`
|
|
4058
|
+
* are content-offset deltas (px, sign = content motion). Native-driven scroll
|
|
4059
|
+
* never reaches the fiber walk, so this is the only way the replay can ride the
|
|
4060
|
+
* scroll instead of freezing on a stale frame. */
|
|
4061
|
+
handleScroll(target, dx, dy) {
|
|
4062
|
+
if (!this.running) return;
|
|
4063
|
+
const leaf = this.currentIndex.get(target);
|
|
4064
|
+
const scrollTag = leaf?.scrollAncestorTag;
|
|
4065
|
+
if (scrollTag === void 0) return;
|
|
4066
|
+
const scrollable = this.currentIndex.get(scrollTag);
|
|
4067
|
+
if (!scrollable) return;
|
|
4068
|
+
const cur = this.scrollOffsets.get(scrollTag) ?? { x: 0, y: 0 };
|
|
4069
|
+
const next = { x: Math.max(0, cur.x + dx), y: Math.max(0, cur.y + dy) };
|
|
4070
|
+
if (Math.abs(next.x - cur.x) < 1 && Math.abs(next.y - cur.y) < 1) return;
|
|
4071
|
+
this.scrollOffsets.set(scrollTag, next);
|
|
4072
|
+
this.recorder.scroll(scrollable.node.key, next.x, next.y, this.now());
|
|
4073
|
+
}
|
|
4074
|
+
/** Public best-effort flush — used by the shared RN error handler to push the
|
|
4075
|
+
* pre-crash tail (the in-class ErrorUtils hook covers apps that don't call
|
|
4076
|
+
* setupRNErrorHandler; both are idempotent via the `flushing` guard). */
|
|
4077
|
+
async flushNow() {
|
|
4078
|
+
await this.flush();
|
|
4079
|
+
}
|
|
4080
|
+
async flush() {
|
|
4081
|
+
if (!this.cfg || this.flushing) return;
|
|
4082
|
+
this.recorder.advance(this.now());
|
|
4083
|
+
const from = this.sentCount;
|
|
4084
|
+
const batch = this.recorder.sliceStream(from);
|
|
4085
|
+
if (batch.length === 0) return;
|
|
4086
|
+
this.flushing = true;
|
|
4087
|
+
const vp = this.recorder.viewportSize;
|
|
4088
|
+
try {
|
|
4089
|
+
const headers = {
|
|
4090
|
+
"content-type": "application/json",
|
|
4091
|
+
"X-API-Key": this.cfg.apiKey
|
|
4092
|
+
};
|
|
4093
|
+
if (this.cfg.projectId) headers["X-Project-ID"] = this.cfg.projectId;
|
|
4094
|
+
const res = await fetch(`${this.cfg.endpoint.replace(/\/$/, "")}/api/replay-stream`, {
|
|
4095
|
+
method: "POST",
|
|
4096
|
+
headers,
|
|
4097
|
+
body: JSON.stringify({
|
|
4098
|
+
session_id: this.sessionId,
|
|
4099
|
+
platform: this.cfg.platform,
|
|
4100
|
+
viewport: { w: vp.w, h: vp.h },
|
|
4101
|
+
seq_start: from,
|
|
4102
|
+
messages: batch
|
|
4103
|
+
})
|
|
4104
|
+
});
|
|
4105
|
+
if (res.ok) {
|
|
4106
|
+
this.sentCount = from + batch.length;
|
|
4107
|
+
if (this.cfg.debug) console.log("[structural] flushed", batch.length, "msgs @", from);
|
|
4108
|
+
} else if (this.cfg.debug) {
|
|
4109
|
+
console.log("[structural] flush rejected", res.status, "\u2014 will retry");
|
|
4110
|
+
}
|
|
4111
|
+
} catch (e) {
|
|
4112
|
+
if (this.cfg.debug) console.log("[structural] flush failed", String(e), "\u2014 buffered");
|
|
4113
|
+
} finally {
|
|
4114
|
+
this.flushing = false;
|
|
4181
4115
|
}
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
if (isOverlayRecord(prev.get(m[1])?.tag, prev.get(m[1])?.attrs)) overlayRemove = true;
|
|
4196
|
-
break;
|
|
4197
|
-
case 10:
|
|
4198
|
-
move++;
|
|
4199
|
-
break;
|
|
4200
|
-
case 12:
|
|
4201
|
-
case 13:
|
|
4202
|
-
if (!newIds.has(m[1])) attr++;
|
|
4203
|
-
break;
|
|
4204
|
-
case 14:
|
|
4205
|
-
if (!newIds.has(m[1])) text++;
|
|
4206
|
-
break;
|
|
4116
|
+
}
|
|
4117
|
+
async stop() {
|
|
4118
|
+
if (!this.running) return;
|
|
4119
|
+
this.recorder.finalize(this.now());
|
|
4120
|
+
if (this.sampleTimer) clearInterval(this.sampleTimer);
|
|
4121
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
4122
|
+
this.sampleTimer = null;
|
|
4123
|
+
this.flushTimer = null;
|
|
4124
|
+
this.running = false;
|
|
4125
|
+
for (const t of this.teardowns.splice(0)) {
|
|
4126
|
+
try {
|
|
4127
|
+
t();
|
|
4128
|
+
} catch {
|
|
4207
4129
|
}
|
|
4208
4130
|
}
|
|
4209
|
-
|
|
4210
|
-
this.analyzer.recordActivity(activity);
|
|
4211
|
-
this.index = index;
|
|
4212
|
-
this.drainPending(t);
|
|
4131
|
+
await this.flush();
|
|
4213
4132
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4133
|
+
};
|
|
4134
|
+
var structuralCapture = new StructuralCapture();
|
|
4135
|
+
|
|
4136
|
+
// src/react-native/error-handler.ts
|
|
4137
|
+
function setupRNErrorHandler(options = {}) {
|
|
4138
|
+
const {
|
|
4139
|
+
captureFatal = true,
|
|
4140
|
+
captureNonFatal = true,
|
|
4141
|
+
capturePromiseRejections = true
|
|
4142
|
+
} = options;
|
|
4143
|
+
const teardowns = [];
|
|
4144
|
+
if (typeof ErrorUtils !== "undefined") {
|
|
4145
|
+
const previousHandler = ErrorUtils.getGlobalHandler();
|
|
4146
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
4147
|
+
if (isFatal && captureFatal || !isFatal && captureNonFatal) {
|
|
4148
|
+
captureError(error, {
|
|
4149
|
+
tags: {
|
|
4150
|
+
fatal: String(isFatal),
|
|
4151
|
+
handler: "react-native-global"
|
|
4152
|
+
}
|
|
4153
|
+
});
|
|
4154
|
+
flushScreenRecording().catch(() => {
|
|
4155
|
+
});
|
|
4156
|
+
if (structuralCapture.isRunning()) {
|
|
4157
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
if (previousHandler) {
|
|
4162
|
+
previousHandler(error, isFatal);
|
|
4163
|
+
}
|
|
4164
|
+
});
|
|
4165
|
+
teardowns.push(() => {
|
|
4166
|
+
if (previousHandler) {
|
|
4167
|
+
ErrorUtils.setGlobalHandler(previousHandler);
|
|
4168
|
+
}
|
|
4169
|
+
});
|
|
4224
4170
|
}
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4171
|
+
if (capturePromiseRejections) {
|
|
4172
|
+
if (typeof global !== "undefined") {
|
|
4173
|
+
const rejectionHandler = (event) => {
|
|
4174
|
+
const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
4175
|
+
captureError(err, {
|
|
4176
|
+
tags: { handler: "unhandled-promise-rejection" }
|
|
4177
|
+
});
|
|
4178
|
+
flushScreenRecording().catch(() => {
|
|
4179
|
+
});
|
|
4180
|
+
if (structuralCapture.isRunning()) {
|
|
4181
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
if (typeof globalThis.addEventListener === "function") {
|
|
4186
|
+
globalThis.addEventListener("unhandledrejection", rejectionHandler);
|
|
4187
|
+
teardowns.push(() => {
|
|
4188
|
+
globalThis.removeEventListener("unhandledrejection", rejectionHandler);
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4229
4192
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4193
|
+
return () => {
|
|
4194
|
+
for (const teardown of teardowns) {
|
|
4195
|
+
teardown();
|
|
4196
|
+
}
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
// src/react-native/navigation.ts
|
|
4201
|
+
function createNavigationTracker(navigationRef, options = {}) {
|
|
4202
|
+
const {
|
|
4203
|
+
trackScreenViews = true,
|
|
4204
|
+
addBreadcrumbs = true,
|
|
4205
|
+
screenNameFilter
|
|
4206
|
+
} = options;
|
|
4207
|
+
let currentRouteName;
|
|
4208
|
+
const unsubscribe = navigationRef.addListener("state", () => {
|
|
4209
|
+
const route = navigationRef.getCurrentRoute();
|
|
4210
|
+
if (!route) return;
|
|
4211
|
+
const previousRouteName = currentRouteName;
|
|
4212
|
+
currentRouteName = route.name;
|
|
4213
|
+
if (previousRouteName === currentRouteName) return;
|
|
4214
|
+
const screenName = screenNameFilter ? screenNameFilter(currentRouteName) : currentRouteName;
|
|
4215
|
+
if (!screenName) return;
|
|
4216
|
+
setCurrentScreen(screenName);
|
|
4217
|
+
if (trackScreenViews) {
|
|
4218
|
+
track("$screen_view", {
|
|
4219
|
+
screen_name: screenName,
|
|
4220
|
+
previous_screen: previousRouteName,
|
|
4221
|
+
params: route.params
|
|
4222
|
+
});
|
|
4223
|
+
}
|
|
4224
|
+
if (addBreadcrumbs) {
|
|
4225
|
+
addBreadcrumb({
|
|
4226
|
+
type: "navigation",
|
|
4227
|
+
category: "screen",
|
|
4228
|
+
message: `Navigated to ${screenName}`,
|
|
4229
|
+
data: {
|
|
4230
|
+
from: previousRouteName,
|
|
4231
|
+
to: screenName
|
|
4232
|
+
}
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
return () => {
|
|
4237
|
+
unsubscribe();
|
|
4238
|
+
setCurrentScreen(void 0);
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
// src/react-native/network.ts
|
|
4243
|
+
function setupNetworkInterception(options = {}) {
|
|
4244
|
+
const {
|
|
4245
|
+
addBreadcrumbs = true,
|
|
4246
|
+
ignoreUrls = [/sitepong\.com/],
|
|
4247
|
+
maxDataSize = 1024
|
|
4248
|
+
} = options;
|
|
4249
|
+
if (typeof XMLHttpRequest === "undefined") {
|
|
4250
|
+
return () => {
|
|
4251
|
+
};
|
|
4236
4252
|
}
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4253
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
4254
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
4255
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
4256
|
+
const xhr = this;
|
|
4257
|
+
xhr._sitepong_method = method;
|
|
4258
|
+
xhr._sitepong_url = String(url);
|
|
4259
|
+
return originalOpen.apply(this, [method, url, ...rest]);
|
|
4260
|
+
};
|
|
4261
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
4262
|
+
const xhr = this;
|
|
4263
|
+
const url = xhr._sitepong_url || "";
|
|
4264
|
+
const method = xhr._sitepong_method || "GET";
|
|
4265
|
+
const shouldIgnore = ignoreUrls.some((pattern) => {
|
|
4266
|
+
if (typeof pattern === "string") return url.includes(pattern);
|
|
4267
|
+
return pattern.test(url);
|
|
4268
|
+
});
|
|
4269
|
+
if (shouldIgnore) {
|
|
4270
|
+
return originalSend.call(this, body);
|
|
4242
4271
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4272
|
+
xhr._sitepong_startTime = Date.now();
|
|
4273
|
+
const onLoadEnd = () => {
|
|
4274
|
+
const duration = xhr._sitepong_startTime ? Date.now() - xhr._sitepong_startTime : void 0;
|
|
4275
|
+
if (addBreadcrumbs) {
|
|
4276
|
+
addBreadcrumb({
|
|
4277
|
+
type: "http",
|
|
4278
|
+
category: "xhr",
|
|
4279
|
+
message: `${method} ${url}`,
|
|
4280
|
+
level: xhr.status >= 400 ? "error" : "info",
|
|
4281
|
+
data: {
|
|
4282
|
+
method,
|
|
4283
|
+
url: url.length > maxDataSize ? url.substring(0, maxDataSize) : url,
|
|
4284
|
+
status_code: xhr.status,
|
|
4285
|
+
duration_ms: duration
|
|
4286
|
+
}
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
};
|
|
4290
|
+
this.addEventListener("loadend", onLoadEnd);
|
|
4291
|
+
return originalSend.call(this, body);
|
|
4292
|
+
};
|
|
4293
|
+
return () => {
|
|
4294
|
+
XMLHttpRequest.prototype.open = originalOpen;
|
|
4295
|
+
XMLHttpRequest.prototype.send = originalSend;
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
var RNAutocaptureModule = class {
|
|
4299
|
+
constructor(options = {}) {
|
|
4300
|
+
this.appStateSubscription = null;
|
|
4301
|
+
this.currentAppState = "active";
|
|
4302
|
+
this.options = {
|
|
4303
|
+
trackAppState: options.trackAppState ?? true
|
|
4304
|
+
};
|
|
4249
4305
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
const ready = this.pending;
|
|
4255
|
-
this.pending = [];
|
|
4256
|
-
for (const p of ready) this.evaluateTap(p);
|
|
4257
|
-
this.stream.sort((a, b) => a.t - b.t);
|
|
4306
|
+
start() {
|
|
4307
|
+
if (this.options.trackAppState) {
|
|
4308
|
+
this.setupAppStateTracking();
|
|
4309
|
+
}
|
|
4258
4310
|
}
|
|
4259
|
-
|
|
4260
|
-
|
|
4311
|
+
stop() {
|
|
4312
|
+
if (this.appStateSubscription) {
|
|
4313
|
+
this.appStateSubscription.remove();
|
|
4314
|
+
this.appStateSubscription = null;
|
|
4315
|
+
}
|
|
4261
4316
|
}
|
|
4262
|
-
|
|
4263
|
-
|
|
4317
|
+
setupAppStateTracking() {
|
|
4318
|
+
try {
|
|
4319
|
+
this.currentAppState = reactNative.AppState.currentState;
|
|
4320
|
+
this.appStateSubscription = reactNative.AppState.addEventListener("change", (nextAppState) => {
|
|
4321
|
+
const previousState = this.currentAppState;
|
|
4322
|
+
this.currentAppState = nextAppState;
|
|
4323
|
+
if (previousState !== nextAppState) {
|
|
4324
|
+
track("$app_state_change", {
|
|
4325
|
+
from: previousState,
|
|
4326
|
+
to: nextAppState
|
|
4327
|
+
});
|
|
4328
|
+
if (previousState.match(/inactive|background/) && nextAppState === "active") {
|
|
4329
|
+
track("$app_foreground", {
|
|
4330
|
+
previous_state: previousState
|
|
4331
|
+
});
|
|
4332
|
+
} else if (previousState === "active" && nextAppState.match(/inactive|background/)) {
|
|
4333
|
+
track("$app_background", {
|
|
4334
|
+
next_state: nextAppState
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
} catch {
|
|
4340
|
+
}
|
|
4264
4341
|
}
|
|
4265
4342
|
};
|
|
4266
4343
|
|
|
4267
|
-
// src/react-native/
|
|
4268
|
-
var
|
|
4269
|
-
var
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
});
|
|
4273
|
-
function record(root) {
|
|
4274
|
-
hookState.latestRoot = root;
|
|
4275
|
-
hookState.dirty = true;
|
|
4344
|
+
// src/react-native/performance.ts
|
|
4345
|
+
var coldStartTimestamp = null;
|
|
4346
|
+
var coldStartTracked = false;
|
|
4347
|
+
function markColdStart() {
|
|
4348
|
+
coldStartTimestamp = Date.now();
|
|
4276
4349
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
if (existing) {
|
|
4281
|
-
const prevRaw = existing.onCommitFiberRoot;
|
|
4282
|
-
const prev = typeof prevRaw === "function" ? prevRaw.bind(existing) : null;
|
|
4283
|
-
existing.onCommitFiberRoot = (id, root, ...rest) => {
|
|
4284
|
-
record(root);
|
|
4285
|
-
return prev ? prev(id, root, ...rest) : void 0;
|
|
4286
|
-
};
|
|
4287
|
-
} else {
|
|
4288
|
-
const renderers = /* @__PURE__ */ new Map();
|
|
4289
|
-
g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
4290
|
-
renderers,
|
|
4291
|
-
supportsFiber: true,
|
|
4292
|
-
inject(renderer) {
|
|
4293
|
-
const id = renderers.size + 1;
|
|
4294
|
-
renderers.set(id, renderer);
|
|
4295
|
-
return id;
|
|
4296
|
-
},
|
|
4297
|
-
onCommitFiberRoot(_id, root) {
|
|
4298
|
-
record(root);
|
|
4299
|
-
},
|
|
4300
|
-
onCommitFiberUnmount() {
|
|
4301
|
-
},
|
|
4302
|
-
onScheduleFiberRoot() {
|
|
4303
|
-
},
|
|
4304
|
-
on() {
|
|
4305
|
-
},
|
|
4306
|
-
off() {
|
|
4307
|
-
},
|
|
4308
|
-
sub() {
|
|
4309
|
-
return () => {
|
|
4310
|
-
};
|
|
4311
|
-
},
|
|
4312
|
-
checkDCE() {
|
|
4313
|
-
}
|
|
4314
|
-
};
|
|
4350
|
+
var RNPerformanceManager = class {
|
|
4351
|
+
constructor() {
|
|
4352
|
+
this.screenRenderStarts = /* @__PURE__ */ new Map();
|
|
4315
4353
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
"
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
"
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
"right",
|
|
4373
|
-
"bottom",
|
|
4374
|
-
"color",
|
|
4375
|
-
"fontSize",
|
|
4376
|
-
"fontWeight",
|
|
4377
|
-
"textAlign",
|
|
4378
|
-
"lineHeight",
|
|
4379
|
-
"gap",
|
|
4380
|
-
"overflow",
|
|
4381
|
-
"shadowColor",
|
|
4382
|
-
"shadowOpacity",
|
|
4383
|
-
"shadowRadius",
|
|
4384
|
-
"elevation",
|
|
4385
|
-
"transform",
|
|
4386
|
-
"zIndex",
|
|
4387
|
-
"aspectRatio"
|
|
4388
|
-
]);
|
|
4389
|
-
function extractStyle(style) {
|
|
4354
|
+
/**
|
|
4355
|
+
* Track cold start time from markColdStart() to now.
|
|
4356
|
+
* Should be called once the app is interactive (e.g., in the root component's useEffect).
|
|
4357
|
+
*/
|
|
4358
|
+
trackColdStart() {
|
|
4359
|
+
if (coldStartTracked || !coldStartTimestamp) return;
|
|
4360
|
+
const duration = Date.now() - coldStartTimestamp;
|
|
4361
|
+
coldStartTracked = true;
|
|
4362
|
+
track("$cold_start", {
|
|
4363
|
+
duration_ms: duration
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Start tracking a screen render. Call before rendering starts.
|
|
4368
|
+
*/
|
|
4369
|
+
startScreenRender(screenName) {
|
|
4370
|
+
this.screenRenderStarts.set(screenName, Date.now());
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* End tracking a screen render. Call after rendering completes.
|
|
4374
|
+
*/
|
|
4375
|
+
endScreenRender(screenName) {
|
|
4376
|
+
const startTime = this.screenRenderStarts.get(screenName);
|
|
4377
|
+
if (startTime === void 0) return null;
|
|
4378
|
+
this.screenRenderStarts.delete(screenName);
|
|
4379
|
+
const duration = Date.now() - startTime;
|
|
4380
|
+
track("$screen_render", {
|
|
4381
|
+
screen_name: screenName,
|
|
4382
|
+
duration_ms: duration
|
|
4383
|
+
});
|
|
4384
|
+
return duration;
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* Track a screen render using a useEffect-friendly pattern.
|
|
4388
|
+
* Returns a cleanup function.
|
|
4389
|
+
*/
|
|
4390
|
+
trackScreenRender(screenName) {
|
|
4391
|
+
this.startScreenRender(screenName);
|
|
4392
|
+
return () => {
|
|
4393
|
+
this.endScreenRender(screenName);
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
4396
|
+
};
|
|
4397
|
+
|
|
4398
|
+
// src/react-native/push.ts
|
|
4399
|
+
var cachedDeviceContext = null;
|
|
4400
|
+
function getEndpoint() {
|
|
4401
|
+
const cfg = sitepong.config;
|
|
4402
|
+
return cfg?.endpoint || "https://ingest.sitepong.com";
|
|
4403
|
+
}
|
|
4404
|
+
function getApiKey() {
|
|
4405
|
+
const cfg = sitepong.config;
|
|
4406
|
+
return cfg?.apiKey || "";
|
|
4407
|
+
}
|
|
4408
|
+
function getDeviceContext() {
|
|
4409
|
+
if (cachedDeviceContext) return cachedDeviceContext;
|
|
4390
4410
|
try {
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4411
|
+
const info = collectDeviceInfo();
|
|
4412
|
+
cachedDeviceContext = {
|
|
4413
|
+
platform: info.platform,
|
|
4414
|
+
appVersion: info.appVersion,
|
|
4415
|
+
deviceModel: info.deviceModel,
|
|
4416
|
+
osVersion: info.osVersion
|
|
4417
|
+
};
|
|
4418
|
+
fetchPersistentDeviceId().then((id) => {
|
|
4419
|
+
if (id && cachedDeviceContext) cachedDeviceContext.deviceId = id;
|
|
4420
|
+
});
|
|
4396
4421
|
} catch {
|
|
4397
|
-
|
|
4422
|
+
cachedDeviceContext = {};
|
|
4398
4423
|
}
|
|
4424
|
+
return cachedDeviceContext;
|
|
4399
4425
|
}
|
|
4400
|
-
function
|
|
4401
|
-
const
|
|
4402
|
-
|
|
4403
|
-
if (
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4426
|
+
async function postToIngest(path, body) {
|
|
4427
|
+
const endpoint = getEndpoint();
|
|
4428
|
+
const apiKey = getApiKey();
|
|
4429
|
+
if (!apiKey) {
|
|
4430
|
+
console.warn("[SitePong] Cannot register push token: SDK not initialized");
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4433
|
+
try {
|
|
4434
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
4435
|
+
method: "POST",
|
|
4436
|
+
headers: {
|
|
4437
|
+
"Content-Type": "application/json",
|
|
4438
|
+
"X-API-Key": apiKey
|
|
4439
|
+
},
|
|
4440
|
+
body: JSON.stringify(body)
|
|
4441
|
+
});
|
|
4442
|
+
if (!response.ok) {
|
|
4443
|
+
console.warn(`[SitePong] Push token registration failed: HTTP ${response.status}`);
|
|
4444
|
+
}
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
console.warn("[SitePong] Push token registration failed:", err);
|
|
4413
4447
|
}
|
|
4414
|
-
if (componentName2) attrs.rnComponent = componentName2;
|
|
4415
|
-
if (props.wtOverlay || props.accessibilityViewIsModal) attrs.wtOverlay = "true";
|
|
4416
|
-
if (props.wtSensitive || props.secureTextEntry) attrs.wtSensitive = "true";
|
|
4417
|
-
return attrs;
|
|
4418
4448
|
}
|
|
4419
|
-
function
|
|
4420
|
-
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4449
|
+
async function deleteFromIngest(path, body) {
|
|
4450
|
+
const endpoint = getEndpoint();
|
|
4451
|
+
const apiKey = getApiKey();
|
|
4452
|
+
if (!apiKey) return;
|
|
4453
|
+
try {
|
|
4454
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
4455
|
+
method: "DELETE",
|
|
4456
|
+
headers: {
|
|
4457
|
+
"Content-Type": "application/json",
|
|
4458
|
+
"X-API-Key": apiKey
|
|
4459
|
+
},
|
|
4460
|
+
body: JSON.stringify(body)
|
|
4461
|
+
});
|
|
4462
|
+
if (!response.ok) {
|
|
4463
|
+
console.warn(`[SitePong] Push token deactivation failed: HTTP ${response.status}`);
|
|
4430
4464
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4465
|
+
} catch (err) {
|
|
4466
|
+
console.warn("[SitePong] Push token deactivation failed:", err);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
function registerPushToken(token, options) {
|
|
4470
|
+
const device = getDeviceContext();
|
|
4471
|
+
postToIngest("/api/push/tokens", {
|
|
4472
|
+
expo_push_token: token,
|
|
4473
|
+
environment: options.environment,
|
|
4474
|
+
device_id: device.deviceId,
|
|
4475
|
+
platform: device.platform,
|
|
4476
|
+
app_version: device.appVersion,
|
|
4477
|
+
device_model: device.deviceModel,
|
|
4478
|
+
os_version: device.osVersion
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
function registerDeviceToken(token, options) {
|
|
4482
|
+
const device = getDeviceContext();
|
|
4483
|
+
const tokenType = options.platform === "ios" ? "apns" : "fcm";
|
|
4484
|
+
postToIngest("/api/push/tokens", {
|
|
4485
|
+
native_device_token: token,
|
|
4486
|
+
token_type: tokenType,
|
|
4487
|
+
environment: options.environment,
|
|
4488
|
+
device_id: device.deviceId,
|
|
4489
|
+
platform: options.platform,
|
|
4490
|
+
app_version: device.appVersion,
|
|
4491
|
+
device_model: device.deviceModel,
|
|
4492
|
+
os_version: device.osVersion
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
function registerLiveActivityToken(activityType, activityId, pushToken, options) {
|
|
4496
|
+
const device = getDeviceContext();
|
|
4497
|
+
postToIngest("/api/push/live-activity-tokens", {
|
|
4498
|
+
activity_type: activityType,
|
|
4499
|
+
activity_id: activityId,
|
|
4500
|
+
push_token: pushToken,
|
|
4501
|
+
environment: options.environment,
|
|
4502
|
+
device_id: device.deviceId
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
function registerPushToStartToken(activityType, pushToStartToken, options) {
|
|
4506
|
+
const device = getDeviceContext();
|
|
4507
|
+
postToIngest("/api/push/push-to-start-tokens", {
|
|
4508
|
+
activity_type: activityType,
|
|
4509
|
+
push_to_start_token: pushToStartToken,
|
|
4510
|
+
environment: options.environment,
|
|
4511
|
+
device_id: device.deviceId
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
function endLiveActivity(activityType, activityId) {
|
|
4515
|
+
deleteFromIngest("/api/push/live-activity-tokens", {
|
|
4516
|
+
activity_type: activityType,
|
|
4517
|
+
activity_id: activityId
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
var SitePongRNContext = React2.createContext({
|
|
4521
|
+
isInitialized: false,
|
|
4522
|
+
performanceManager: null
|
|
4523
|
+
});
|
|
4524
|
+
function SitePongRNProvider({
|
|
4525
|
+
apiKey,
|
|
4526
|
+
config = {},
|
|
4527
|
+
asyncStorage,
|
|
4528
|
+
navigationRef,
|
|
4529
|
+
trackNetwork = true,
|
|
4530
|
+
trackAppState = true,
|
|
4531
|
+
captureErrors = true,
|
|
4532
|
+
screenRecording,
|
|
4533
|
+
devicePushToken,
|
|
4534
|
+
devicePlatform,
|
|
4535
|
+
pushEnvironment = "production",
|
|
4536
|
+
pushUserId,
|
|
4537
|
+
children
|
|
4538
|
+
}) {
|
|
4539
|
+
const initialized = React2.useRef(false);
|
|
4540
|
+
const [sdkReady, setSdkReady] = React2.useState(false);
|
|
4541
|
+
const performanceManagerRef = React2.useRef(null);
|
|
4542
|
+
React2.useEffect(() => {
|
|
4543
|
+
if (initialized.current) return;
|
|
4544
|
+
const teardowns = [];
|
|
4545
|
+
const deviceInfo = collectDeviceInfo();
|
|
4546
|
+
setRNDeviceInfo({
|
|
4547
|
+
platform: "react-native",
|
|
4548
|
+
osName: deviceInfo.platform,
|
|
4549
|
+
osVersion: deviceInfo.osVersion,
|
|
4550
|
+
deviceName: deviceInfo.deviceName,
|
|
4551
|
+
deviceBrand: deviceInfo.deviceBrand,
|
|
4552
|
+
deviceModel: deviceInfo.deviceModel,
|
|
4553
|
+
appVersion: deviceInfo.appVersion,
|
|
4554
|
+
screenWidth: deviceInfo.screenWidth,
|
|
4555
|
+
screenHeight: deviceInfo.screenHeight,
|
|
4556
|
+
isEmulator: deviceInfo.isEmulator
|
|
4557
|
+
});
|
|
4558
|
+
fetchPersistentDeviceId().then((id) => {
|
|
4559
|
+
if (id) setNativeDeviceId(id);
|
|
4560
|
+
}).catch(() => {
|
|
4561
|
+
});
|
|
4562
|
+
const storageAdapter = asyncStorage ? createAsyncStorageAdapter(asyncStorage) : void 0;
|
|
4563
|
+
initRN({
|
|
4564
|
+
apiKey,
|
|
4565
|
+
...config,
|
|
4566
|
+
_storageAdapter: storageAdapter
|
|
4567
|
+
});
|
|
4568
|
+
if (captureErrors) {
|
|
4569
|
+
const teardownErrors = setupRNErrorHandler();
|
|
4570
|
+
teardowns.push(teardownErrors);
|
|
4571
|
+
}
|
|
4572
|
+
if (trackNetwork) {
|
|
4573
|
+
const teardownNetwork = setupNetworkInterception();
|
|
4574
|
+
teardowns.push(teardownNetwork);
|
|
4575
|
+
}
|
|
4576
|
+
if (trackAppState) {
|
|
4577
|
+
const autocapture = new RNAutocaptureModule({ trackAppState: true });
|
|
4578
|
+
autocapture.start();
|
|
4579
|
+
teardowns.push(() => autocapture.stop());
|
|
4580
|
+
}
|
|
4581
|
+
if (navigationRef) {
|
|
4582
|
+
const teardownNav = createNavigationTracker(navigationRef);
|
|
4583
|
+
teardowns.push(teardownNav);
|
|
4584
|
+
}
|
|
4585
|
+
if (screenRecording?.enabled) {
|
|
4586
|
+
const sessionId = `rn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
4587
|
+
const endpoint = config.endpoint || "https://ingest.sitepong.com";
|
|
4588
|
+
startScreenRecordingOnError(apiKey, endpoint, sessionId, screenRecording);
|
|
4589
|
+
teardowns.push(() => stopScreenRecording());
|
|
4590
|
+
}
|
|
4591
|
+
if (devicePushToken && devicePlatform) {
|
|
4592
|
+
registerDeviceToken(devicePushToken, { platform: devicePlatform, environment: pushEnvironment});
|
|
4593
|
+
}
|
|
4594
|
+
performanceManagerRef.current = new RNPerformanceManager();
|
|
4595
|
+
performanceManagerRef.current.trackColdStart();
|
|
4596
|
+
initialized.current = true;
|
|
4597
|
+
setSdkReady(true);
|
|
4598
|
+
return () => {
|
|
4599
|
+
for (const teardown of teardowns) {
|
|
4600
|
+
teardown();
|
|
4601
|
+
}
|
|
4602
|
+
};
|
|
4603
|
+
}, [apiKey]);
|
|
4604
|
+
const value = {
|
|
4605
|
+
isInitialized: sdkReady,
|
|
4606
|
+
performanceManager: performanceManagerRef.current
|
|
4433
4607
|
};
|
|
4434
|
-
|
|
4435
|
-
return out;
|
|
4608
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SitePongRNContext.Provider, { value, children });
|
|
4436
4609
|
}
|
|
4437
|
-
function
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4610
|
+
function useSitePongRNContext() {
|
|
4611
|
+
return React2.useContext(SitePongRNContext);
|
|
4612
|
+
}
|
|
4613
|
+
function useScreenTrack(screenName) {
|
|
4614
|
+
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
4615
|
+
React2.useEffect(() => {
|
|
4616
|
+
if (!performanceManager) return;
|
|
4617
|
+
performanceManager.startScreenRender(screenName);
|
|
4618
|
+
return () => {
|
|
4619
|
+
performanceManager.endScreenRender(screenName);
|
|
4620
|
+
};
|
|
4621
|
+
}, [screenName, performanceManager]);
|
|
4445
4622
|
}
|
|
4446
|
-
function
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4623
|
+
function useAppState() {
|
|
4624
|
+
const [appState, setAppState] = React2.useState("active");
|
|
4625
|
+
React2.useEffect(() => {
|
|
4626
|
+
setAppState(reactNative.AppState.currentState);
|
|
4627
|
+
const subscription = reactNative.AppState.addEventListener("change", (nextState) => {
|
|
4628
|
+
setAppState(nextState);
|
|
4629
|
+
});
|
|
4630
|
+
return () => subscription.remove();
|
|
4631
|
+
}, []);
|
|
4632
|
+
return appState;
|
|
4451
4633
|
}
|
|
4452
|
-
function
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
const
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
const myPath = [...parentPath, { tag, attrs, nthOfType }];
|
|
4463
|
-
const children = isTextLeaf ? [] : buildHostChildren(fiber.child, void 0, index, myPath);
|
|
4464
|
-
const vnode = { key, tag, attrs, children, ...text ? { text } : {} };
|
|
4465
|
-
out.push(vnode);
|
|
4466
|
-
const nt = nativeTagOf(stateNode);
|
|
4467
|
-
if (nt !== void 0) index.set(nt, { node: vnode, path: myPath });
|
|
4468
|
-
return;
|
|
4469
|
-
}
|
|
4470
|
-
const name = componentName(fiber) ?? inheritedName;
|
|
4471
|
-
let child = fiber.child;
|
|
4472
|
-
while (child) {
|
|
4473
|
-
collectFrom(child, name, out, index, parentPath);
|
|
4474
|
-
child = child.sibling;
|
|
4475
|
-
}
|
|
4634
|
+
function useRemoteConfig() {
|
|
4635
|
+
const [config, setConfig] = React2.useState(() => getRemoteConfig());
|
|
4636
|
+
React2.useEffect(() => {
|
|
4637
|
+
setConfig(getRemoteConfig());
|
|
4638
|
+
const unsubscribe = onRemoteConfigChange((newConfig) => {
|
|
4639
|
+
setConfig(newConfig);
|
|
4640
|
+
});
|
|
4641
|
+
return unsubscribe;
|
|
4642
|
+
}, []);
|
|
4643
|
+
return config;
|
|
4476
4644
|
}
|
|
4477
|
-
function
|
|
4478
|
-
const
|
|
4479
|
-
const
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4645
|
+
function useRNPerformance(screenName) {
|
|
4646
|
+
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
4647
|
+
const [duration, setDuration] = React2.useState(null);
|
|
4648
|
+
const startTimeRef = React2.useRef(Date.now());
|
|
4649
|
+
React2.useEffect(() => {
|
|
4650
|
+
startTimeRef.current = Date.now();
|
|
4651
|
+
const handle = requestAnimationFrame(() => {
|
|
4652
|
+
const d = Date.now() - startTimeRef.current;
|
|
4653
|
+
setDuration(d);
|
|
4654
|
+
if (performanceManager) {
|
|
4655
|
+
performanceManager.startScreenRender(screenName);
|
|
4656
|
+
performanceManager.endScreenRender(screenName);
|
|
4657
|
+
}
|
|
4658
|
+
});
|
|
4659
|
+
return () => cancelAnimationFrame(handle);
|
|
4660
|
+
}, [screenName, performanceManager]);
|
|
4661
|
+
return { duration };
|
|
4485
4662
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
const rand = Math.floor(Math.random() * 16777215).toString(36);
|
|
4490
|
-
return `wt-${Date.now().toString(36)}-${rand}`;
|
|
4663
|
+
var warnedMissing = false;
|
|
4664
|
+
function loadBridge() {
|
|
4665
|
+
return getScreenRecorderModule();
|
|
4491
4666
|
}
|
|
4492
|
-
var
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
this.sessionId = "";
|
|
4503
|
-
this.running = false;
|
|
4504
|
-
}
|
|
4505
|
-
now() {
|
|
4506
|
-
return Date.now() - this.started;
|
|
4507
|
-
}
|
|
4508
|
-
isRunning() {
|
|
4509
|
-
return this.running;
|
|
4510
|
-
}
|
|
4511
|
-
getSessionId() {
|
|
4512
|
-
return this.sessionId;
|
|
4513
|
-
}
|
|
4514
|
-
start(cfg) {
|
|
4515
|
-
if (this.running) return;
|
|
4516
|
-
installStructuralHook();
|
|
4517
|
-
this.cfg = cfg;
|
|
4518
|
-
this.sessionId = cfg.sessionId || genSessionId();
|
|
4519
|
-
this.started = Date.now();
|
|
4520
|
-
this.running = true;
|
|
4521
|
-
const { width, height } = reactNative.Dimensions.get("window");
|
|
4522
|
-
this.recorder.setViewport(Math.round(width), Math.round(height), 0);
|
|
4523
|
-
this.sampleTimer = setInterval(() => this.sample(), cfg.sampleIntervalMs ?? 120);
|
|
4524
|
-
this.flushTimer = setInterval(() => void this.flush(), cfg.flushIntervalMs ?? 4e3);
|
|
4525
|
-
if (cfg.debug) console.log("[structural] started", this.sessionId);
|
|
4526
|
-
setTimeout(() => this.sample(true), 0);
|
|
4527
|
-
}
|
|
4528
|
-
sample(force = false) {
|
|
4529
|
-
if (!this.running) return;
|
|
4530
|
-
if (!force && !hookState.dirty) return;
|
|
4531
|
-
hookState.dirty = false;
|
|
4532
|
-
const root = hookState.latestRoot;
|
|
4533
|
-
if (!root) return;
|
|
4534
|
-
try {
|
|
4535
|
-
const { tree, index } = walkFiberRoot(root);
|
|
4536
|
-
this.currentIndex = index;
|
|
4537
|
-
this.recorder.commit(tree, this.now());
|
|
4538
|
-
} catch (e) {
|
|
4539
|
-
if (this.cfg?.debug) console.log("[structural] sample error", String(e));
|
|
4667
|
+
var started = false;
|
|
4668
|
+
function startWatchtower(config) {
|
|
4669
|
+
if (started) return;
|
|
4670
|
+
const bridge = loadBridge();
|
|
4671
|
+
if (!bridge) {
|
|
4672
|
+
if (!warnedMissing) {
|
|
4673
|
+
warnedMissing = true;
|
|
4674
|
+
console.warn(
|
|
4675
|
+
"[SitePong] Watchtower is enabled but the native module is unavailable. Rebuild the native app (npx expo prebuild && npx expo run:ios) so the bundled SitePong native module is linked."
|
|
4676
|
+
);
|
|
4540
4677
|
}
|
|
4678
|
+
return;
|
|
4541
4679
|
}
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4680
|
+
bridge.watchtowerStart({
|
|
4681
|
+
apiKey: config.apiKey,
|
|
4682
|
+
projectId: config.projectId,
|
|
4683
|
+
endpoint: config.endpoint,
|
|
4684
|
+
sampleRate: config.sampleRate ?? 0.1,
|
|
4685
|
+
platform: config.platform ?? "react-native-ios"
|
|
4686
|
+
});
|
|
4687
|
+
started = true;
|
|
4688
|
+
if (config.distinctId) setWatchtowerUser(config.distinctId);
|
|
4689
|
+
}
|
|
4690
|
+
function setWatchtowerUser(distinctId) {
|
|
4691
|
+
try {
|
|
4692
|
+
loadBridge()?.watchtowerSetUser(distinctId);
|
|
4693
|
+
} catch {
|
|
4548
4694
|
}
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
const { width, height } = reactNative.Dimensions.get("window");
|
|
4556
|
-
this.recorder.tap(
|
|
4557
|
-
{
|
|
4558
|
-
key: entry.node.key,
|
|
4559
|
-
path: entry.path,
|
|
4560
|
-
attrs: entry.node.attrs,
|
|
4561
|
-
text: entry.node.text,
|
|
4562
|
-
normX: pageX / (width || 1),
|
|
4563
|
-
normY: pageY / (height || 1)
|
|
4564
|
-
},
|
|
4565
|
-
this.now()
|
|
4566
|
-
);
|
|
4695
|
+
}
|
|
4696
|
+
function getWatchtowerSessionId() {
|
|
4697
|
+
try {
|
|
4698
|
+
return loadBridge()?.watchtowerGetSessionId() ?? null;
|
|
4699
|
+
} catch {
|
|
4700
|
+
return null;
|
|
4567
4701
|
}
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
headers,
|
|
4583
|
-
body: JSON.stringify({
|
|
4584
|
-
session_id: this.sessionId,
|
|
4585
|
-
platform: this.cfg.platform,
|
|
4586
|
-
viewport: { w: vp.w, h: vp.h },
|
|
4587
|
-
messages
|
|
4588
|
-
})
|
|
4589
|
-
});
|
|
4590
|
-
if (this.cfg.debug) console.log("[structural] flushed", messages.length, "messages");
|
|
4591
|
-
} catch (e) {
|
|
4592
|
-
if (this.cfg.debug) console.log("[structural] flush failed", String(e));
|
|
4702
|
+
}
|
|
4703
|
+
function stopWatchtower() {
|
|
4704
|
+
if (!started) return;
|
|
4705
|
+
loadBridge()?.watchtowerStop();
|
|
4706
|
+
started = false;
|
|
4707
|
+
}
|
|
4708
|
+
function subscribeNavigationToWatchtower(navigationRef) {
|
|
4709
|
+
const unsubscribe = createNavigationTracker(navigationRef, {
|
|
4710
|
+
screenNameFilter: (name) => {
|
|
4711
|
+
try {
|
|
4712
|
+
loadBridge()?.watchtowerSetScreen(name);
|
|
4713
|
+
} catch {
|
|
4714
|
+
}
|
|
4715
|
+
return name;
|
|
4593
4716
|
}
|
|
4717
|
+
});
|
|
4718
|
+
try {
|
|
4719
|
+
const route = navigationRef.getCurrentRoute?.();
|
|
4720
|
+
if (route?.name) loadBridge()?.watchtowerSetScreen(route.name);
|
|
4721
|
+
} catch {
|
|
4594
4722
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
};
|
|
4606
|
-
|
|
4723
|
+
return unsubscribe;
|
|
4724
|
+
}
|
|
4725
|
+
function WatchtowerProvider(props) {
|
|
4726
|
+
const { config, navigationRef, children } = props;
|
|
4727
|
+
React2__default.default.useEffect(() => {
|
|
4728
|
+
startWatchtower(config);
|
|
4729
|
+
const unsubscribe = subscribeNavigationToWatchtower(navigationRef);
|
|
4730
|
+
return () => {
|
|
4731
|
+
unsubscribe();
|
|
4732
|
+
stopWatchtower();
|
|
4733
|
+
};
|
|
4734
|
+
}, []);
|
|
4735
|
+
return React2__default.default.createElement(React2__default.default.Fragment, null, children);
|
|
4736
|
+
}
|
|
4607
4737
|
var TAP_MOVE_THRESHOLD_PX = 12;
|
|
4608
4738
|
function StructuralCaptureProvider({ children, ...cfg }) {
|
|
4739
|
+
const cfgRef = React2.useRef(cfg);
|
|
4740
|
+
cfgRef.current = cfg;
|
|
4609
4741
|
React2.useEffect(() => {
|
|
4610
|
-
structuralCapture.start(
|
|
4742
|
+
structuralCapture.start(cfgRef.current);
|
|
4611
4743
|
return () => {
|
|
4612
4744
|
void structuralCapture.stop();
|
|
4613
4745
|
};
|
|
4614
4746
|
}, []);
|
|
4615
|
-
const
|
|
4747
|
+
const gesture = React2.useRef(null);
|
|
4616
4748
|
const onTouchStart = (e) => {
|
|
4617
|
-
const
|
|
4618
|
-
|
|
4749
|
+
const ne = e.nativeEvent;
|
|
4750
|
+
gesture.current = {
|
|
4751
|
+
startX: ne.pageX,
|
|
4752
|
+
startY: ne.pageY,
|
|
4753
|
+
lastX: ne.pageX,
|
|
4754
|
+
lastY: ne.pageY,
|
|
4755
|
+
target: Number(ne.target),
|
|
4756
|
+
scrolling: false
|
|
4757
|
+
};
|
|
4758
|
+
};
|
|
4759
|
+
const onTouchMove = (e) => {
|
|
4760
|
+
const g2 = gesture.current;
|
|
4761
|
+
if (!g2) return;
|
|
4762
|
+
const ne = e.nativeEvent;
|
|
4763
|
+
const movedFar = Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX;
|
|
4764
|
+
if (movedFar) g2.scrolling = true;
|
|
4765
|
+
if (g2.scrolling) {
|
|
4766
|
+
const dx = g2.lastX - ne.pageX;
|
|
4767
|
+
const dy = g2.lastY - ne.pageY;
|
|
4768
|
+
structuralCapture.handleScroll(g2.target, dx, dy);
|
|
4769
|
+
}
|
|
4770
|
+
g2.lastX = ne.pageX;
|
|
4771
|
+
g2.lastY = ne.pageY;
|
|
4619
4772
|
};
|
|
4620
4773
|
const onTouchEnd = (e) => {
|
|
4774
|
+
const g2 = gesture.current;
|
|
4775
|
+
gesture.current = null;
|
|
4776
|
+
if (!g2) return;
|
|
4777
|
+
if (g2.scrolling) return;
|
|
4621
4778
|
const ne = e.nativeEvent;
|
|
4622
|
-
|
|
4623
|
-
startPos.current = null;
|
|
4624
|
-
if (!s) return;
|
|
4625
|
-
if (Math.abs(ne.pageX - s.x) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - s.y) > TAP_MOVE_THRESHOLD_PX) {
|
|
4779
|
+
if (Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX) {
|
|
4626
4780
|
return;
|
|
4627
4781
|
}
|
|
4628
|
-
structuralCapture.handleTap(
|
|
4782
|
+
structuralCapture.handleTap(g2.target, ne.pageX, ne.pageY);
|
|
4629
4783
|
};
|
|
4630
|
-
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, onTouchStart, onTouchEnd, children });
|
|
4784
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, onTouchStart, onTouchMove, onTouchEnd, children });
|
|
4631
4785
|
}
|
|
4632
4786
|
function useStructuralScreen(name) {
|
|
4633
4787
|
React2.useEffect(() => {
|