sitepong 0.2.8 → 0.2.10
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 +28 -0
- package/dist/entries/rn.js +1397 -1205
- 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,1319 +3315,1511 @@ function SensitiveView(props) {
|
|
|
3315
3315
|
);
|
|
3316
3316
|
}
|
|
3317
3317
|
|
|
3318
|
-
//
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
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;
|
|
3318
|
+
// ../watchtower-recorder/dist/index.mjs
|
|
3319
|
+
var CreateElementNode = (id, parentID, index, tag) => [8, id, parentID, index, tag];
|
|
3320
|
+
var CreateTextNode = (id, parentID, index) => [
|
|
3321
|
+
9,
|
|
3322
|
+
id,
|
|
3323
|
+
parentID,
|
|
3324
|
+
index
|
|
3325
|
+
];
|
|
3326
|
+
var MoveNode = (id, parentID, index) => [
|
|
3327
|
+
10,
|
|
3328
|
+
id,
|
|
3329
|
+
parentID,
|
|
3330
|
+
index
|
|
3331
|
+
];
|
|
3332
|
+
var RemoveNode = (id) => [11, id];
|
|
3333
|
+
var SetNodeAttribute = (id, name, value) => [
|
|
3334
|
+
12,
|
|
3335
|
+
id,
|
|
3336
|
+
name,
|
|
3337
|
+
value
|
|
3338
|
+
];
|
|
3339
|
+
var RemoveNodeAttribute = (id, name) => [
|
|
3340
|
+
13,
|
|
3341
|
+
id,
|
|
3342
|
+
name
|
|
3343
|
+
];
|
|
3344
|
+
var SetNodeData = (id, data) => [14, id, data];
|
|
3345
|
+
var SetNodeScroll = (id, x, y) => [
|
|
3346
|
+
16,
|
|
3347
|
+
id,
|
|
3348
|
+
x,
|
|
3349
|
+
y
|
|
3350
|
+
];
|
|
3351
|
+
var MobileTap = (id, hesitationMs, label, selector, normX, normY) => [68, id, hesitationMs, label, selector, normX, normY];
|
|
3352
|
+
var ScreenView = (name, prevName) => [
|
|
3353
|
+
200,
|
|
3354
|
+
name,
|
|
3355
|
+
prevName
|
|
3356
|
+
];
|
|
3357
|
+
var Frustration = (kind, nodeId, selector, count) => [201, kind, nodeId, selector, count];
|
|
3358
|
+
var TapOutcome = (tapNodeId, kind, label, mutationCount) => [202, tapNodeId, kind, label, mutationCount];
|
|
3359
|
+
function walk(root, visit) {
|
|
3360
|
+
const rec = (node, parent, index) => {
|
|
3361
|
+
visit(node, parent, index);
|
|
3362
|
+
node.children.forEach((child, i) => rec(child, node, i));
|
|
3470
3363
|
};
|
|
3364
|
+
if (root) rec(root, null, 0);
|
|
3471
3365
|
}
|
|
3472
|
-
var
|
|
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 {
|
|
3366
|
+
var Mirror = class {
|
|
3525
3367
|
constructor() {
|
|
3526
|
-
this
|
|
3368
|
+
__publicField(this, "ids", /* @__PURE__ */ new WeakMap());
|
|
3369
|
+
__publicField(this, "byId", /* @__PURE__ */ new Map());
|
|
3370
|
+
__publicField(this, "next", 1);
|
|
3527
3371
|
}
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
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
|
-
});
|
|
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);
|
|
3539
3376
|
}
|
|
3540
|
-
/**
|
|
3541
|
-
*
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
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];
|
|
3545
3386
|
}
|
|
3546
|
-
|
|
3547
|
-
|
|
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;
|
|
3387
|
+
getKey(id) {
|
|
3388
|
+
return this.byId.get(id);
|
|
3559
3389
|
}
|
|
3560
|
-
/**
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
this.
|
|
3566
|
-
return
|
|
3567
|
-
|
|
3568
|
-
|
|
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;
|
|
3569
3400
|
}
|
|
3570
3401
|
};
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
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;
|
|
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
|
|
3594
3416
|
});
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
}
|
|
3598
|
-
return cachedDeviceContext;
|
|
3417
|
+
order.push(id);
|
|
3418
|
+
});
|
|
3419
|
+
return { next, order };
|
|
3599
3420
|
}
|
|
3600
|
-
|
|
3601
|
-
const
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
console.warn("[SitePong] Cannot register push token: SDK not initialized");
|
|
3605
|
-
return;
|
|
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]));
|
|
3606
3425
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
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);
|
|
3426
|
+
for (const k of Object.keys(prev)) {
|
|
3427
|
+
if (!(k in next)) out.push(RemoveNodeAttribute(id, k));
|
|
3621
3428
|
}
|
|
3429
|
+
return out;
|
|
3622
3430
|
}
|
|
3623
|
-
|
|
3624
|
-
const
|
|
3625
|
-
const
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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);
|
|
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));
|
|
3641
3439
|
}
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
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);
|
|
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));
|
|
3831
3448
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
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 };
|
|
3836
3460
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
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;
|
|
3840
3465
|
}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
const
|
|
3845
|
-
if (
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
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})`);
|
|
3851
3487
|
}
|
|
3852
|
-
return;
|
|
3853
3488
|
}
|
|
3854
|
-
|
|
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);
|
|
3489
|
+
return parts.join(" > ");
|
|
3863
3490
|
}
|
|
3864
|
-
function
|
|
3865
|
-
|
|
3866
|
-
loadBridge()?.watchtowerSetUser(distinctId);
|
|
3867
|
-
} catch {
|
|
3868
|
-
}
|
|
3491
|
+
function truncate(s, n = 40) {
|
|
3492
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
3869
3493
|
}
|
|
3870
|
-
function
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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 "";
|
|
3876
3500
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
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;
|
|
3881
3568
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
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;
|
|
3888
3619
|
}
|
|
3889
|
-
return name;
|
|
3890
3620
|
}
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
}
|
|
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);
|
|
3636
|
+
}
|
|
3637
|
+
screenView(name, prevName, t) {
|
|
3638
|
+
this.emit(t, ScreenView(name, prevName));
|
|
3639
|
+
this.analyzer.recordScreenView(t);
|
|
3640
|
+
this.drainPending(t);
|
|
3641
|
+
}
|
|
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)));
|
|
3648
|
+
}
|
|
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]);
|
|
3652
|
+
}
|
|
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));
|
|
3896
3667
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
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
|
-
// ../watchtower-recorder/dist/index.mjs
|
|
3913
|
-
var CreateElementNode = (id, parentID, index, tag) => [8, id, parentID, index, tag];
|
|
3914
|
-
var CreateTextNode = (id, parentID, index) => [
|
|
3915
|
-
9,
|
|
3916
|
-
id,
|
|
3917
|
-
parentID,
|
|
3918
|
-
index
|
|
3919
|
-
];
|
|
3920
|
-
var MoveNode = (id, parentID, index) => [
|
|
3921
|
-
10,
|
|
3922
|
-
id,
|
|
3923
|
-
parentID,
|
|
3924
|
-
index
|
|
3925
|
-
];
|
|
3926
|
-
var RemoveNode = (id) => [11, id];
|
|
3927
|
-
var SetNodeAttribute = (id, name, value) => [
|
|
3928
|
-
12,
|
|
3929
|
-
id,
|
|
3930
|
-
name,
|
|
3931
|
-
value
|
|
3932
|
-
];
|
|
3933
|
-
var RemoveNodeAttribute = (id, name) => [
|
|
3934
|
-
13,
|
|
3935
|
-
id,
|
|
3936
|
-
name
|
|
3937
|
-
];
|
|
3938
|
-
var SetNodeData = (id, data) => [14, id, data];
|
|
3939
|
-
var MobileTap = (id, hesitationMs, label, selector, normX, normY) => [68, id, hesitationMs, label, selector, normX, normY];
|
|
3940
|
-
var ScreenView = (name, prevName) => [
|
|
3941
|
-
200,
|
|
3942
|
-
name,
|
|
3943
|
-
prevName
|
|
3944
|
-
];
|
|
3945
|
-
var Frustration = (kind, nodeId, selector, count) => [201, kind, nodeId, selector, count];
|
|
3946
|
-
var TapOutcome = (tapNodeId, kind, label, mutationCount) => [202, tapNodeId, kind, label, mutationCount];
|
|
3947
|
-
function walk(root, visit) {
|
|
3948
|
-
const rec = (node, parent, index) => {
|
|
3949
|
-
visit(node, parent, index);
|
|
3950
|
-
node.children.forEach((child, i) => rec(child, node, i));
|
|
3951
|
-
};
|
|
3952
|
-
if (root) rec(root, null, 0);
|
|
3953
|
-
}
|
|
3954
|
-
var Mirror = class {
|
|
3955
|
-
constructor() {
|
|
3956
|
-
__publicField(this, "ids", /* @__PURE__ */ new WeakMap());
|
|
3957
|
-
__publicField(this, "byId", /* @__PURE__ */ new Map());
|
|
3958
|
-
__publicField(this, "next", 1);
|
|
3668
|
+
/** Explicit clock advance (adapter ticker) to close outcome windows even when
|
|
3669
|
+
* no commit/tap arrives. */
|
|
3670
|
+
advance(t) {
|
|
3671
|
+
this.drainPending(t);
|
|
3959
3672
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
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);
|
|
3964
3681
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
getId(key) {
|
|
3968
|
-
const existing = this.ids.get(key);
|
|
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];
|
|
3682
|
+
getStream() {
|
|
3683
|
+
return [...this.stream].sort((a, b) => a.t - b.t);
|
|
3974
3684
|
}
|
|
3975
|
-
|
|
3976
|
-
|
|
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;
|
|
3977
3689
|
}
|
|
3978
|
-
/**
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
return
|
|
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));
|
|
3985
3697
|
}
|
|
3986
|
-
get
|
|
3987
|
-
return this.
|
|
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
|
-
function
|
|
4083
|
-
if (
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
if (
|
|
4087
|
-
|
|
3923
|
+
function fiberRootFromInstance(instance) {
|
|
3924
|
+
if (!instance || typeof instance !== "object") return null;
|
|
3925
|
+
const inst = instance;
|
|
3926
|
+
const fiber = inst.__internalInstanceHandle ?? inst._internalFiberInstanceHandleDEV ?? null;
|
|
3927
|
+
if (!fiber || typeof fiber !== "object") return null;
|
|
3928
|
+
let top = fiber;
|
|
3929
|
+
for (let guard = 0; top.return && guard < 1e5; guard++) top = top.return;
|
|
3930
|
+
const fiberRoot = top.stateNode;
|
|
3931
|
+
return fiberRoot && typeof fiberRoot === "object" && fiberRoot.current ? fiberRoot : null;
|
|
4088
3932
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
var
|
|
4092
|
-
var
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
content_changed: "Updated on-screen content",
|
|
4099
|
-
no_effect: "Nothing changed (dead tap)"
|
|
4100
|
-
};
|
|
4101
|
-
var InteractionAnalyzer = class {
|
|
3933
|
+
|
|
3934
|
+
// src/react-native/structural/capture.ts
|
|
3935
|
+
var WALK_BUDGET_MS = 24;
|
|
3936
|
+
var MAX_OVERLOAD_SKIP = 8;
|
|
3937
|
+
function genSessionId() {
|
|
3938
|
+
const rand = Math.floor(Math.random() * 16777215).toString(36);
|
|
3939
|
+
return `wt-${Date.now().toString(36)}-${rand}`;
|
|
3940
|
+
}
|
|
3941
|
+
var StructuralCapture = class {
|
|
4102
3942
|
constructor() {
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
3943
|
+
this.recorder = new Recorder();
|
|
3944
|
+
this.currentIndex = /* @__PURE__ */ new Map();
|
|
3945
|
+
this.started = 0;
|
|
3946
|
+
this.sampleTimer = null;
|
|
3947
|
+
this.flushTimer = null;
|
|
3948
|
+
this.screen = "";
|
|
3949
|
+
this.prevScreen = "";
|
|
3950
|
+
this.cfg = null;
|
|
3951
|
+
this.sessionId = "";
|
|
3952
|
+
this.running = false;
|
|
3953
|
+
// Incremental delivery: cursor into the recorder's emission stream that has
|
|
3954
|
+
// been CONFIRMED stored. Everything at index >= sentCount is unsent (the
|
|
3955
|
+
// offline buffer). Advances only on a 2xx.
|
|
3956
|
+
this.sentCount = 0;
|
|
3957
|
+
this.flushing = false;
|
|
3958
|
+
// Persistent per-ScrollView content offset (keyed by native tag), so scroll
|
|
3959
|
+
// continues from where it was rather than resetting each gesture.
|
|
3960
|
+
this.scrollOffsets = /* @__PURE__ */ new Map();
|
|
3961
|
+
// Adaptive sampling: skip this many upcoming ticks after a heavy walk.
|
|
3962
|
+
this.overloadSkip = 0;
|
|
3963
|
+
// FiberRoot acquired from the provider's root-View ref (fiber.ts). This is the
|
|
3964
|
+
// production path: Release builds strip the React DevTools commit hook, so
|
|
3965
|
+
// `hookState.latestRoot` never populates and hook-based capture emits only the
|
|
3966
|
+
// initial viewport message. The FiberRoot persists for the app lifetime and
|
|
3967
|
+
// walkFiberRoot reads its live `.current` each sample, so we hold it here and
|
|
3968
|
+
// walk every tick (no `dirty` signal exists without the hook).
|
|
3969
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3970
|
+
this.explicitRoot = null;
|
|
3971
|
+
this.appStateSub = null;
|
|
3972
|
+
this.teardowns = [];
|
|
4106
3973
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
3974
|
+
now() {
|
|
3975
|
+
return Date.now() - this.started;
|
|
4109
3976
|
}
|
|
4110
|
-
|
|
4111
|
-
this.
|
|
3977
|
+
isRunning() {
|
|
3978
|
+
return this.running;
|
|
4112
3979
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
3980
|
+
getSessionId() {
|
|
3981
|
+
return this.sessionId;
|
|
3982
|
+
}
|
|
3983
|
+
start(cfg) {
|
|
3984
|
+
if (this.running) return;
|
|
3985
|
+
installStructuralHook();
|
|
3986
|
+
this.cfg = cfg;
|
|
3987
|
+
this.sessionId = cfg.sessionId || genSessionId();
|
|
3988
|
+
this.started = Date.now();
|
|
3989
|
+
this.running = true;
|
|
3990
|
+
this.sentCount = 0;
|
|
3991
|
+
this.scrollOffsets.clear();
|
|
3992
|
+
this.overloadSkip = 0;
|
|
3993
|
+
this.explicitRoot = null;
|
|
3994
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
3995
|
+
this.recorder.setViewport(Math.round(width), Math.round(height), 0);
|
|
3996
|
+
this.sampleTimer = setInterval(() => this.sample(), cfg.sampleIntervalMs ?? 120);
|
|
3997
|
+
this.flushTimer = setInterval(() => void this.flush(), cfg.flushIntervalMs ?? 4e3);
|
|
3998
|
+
this.installCrashFlush();
|
|
3999
|
+
this.installBackgroundFlush();
|
|
4000
|
+
if (cfg.debug) console.log("[structural] started", this.sessionId);
|
|
4001
|
+
setTimeout(() => this.sample(true), 0);
|
|
4002
|
+
}
|
|
4003
|
+
/** Best-effort flush the pre-crash tail before the app tears down. */
|
|
4004
|
+
installCrashFlush() {
|
|
4005
|
+
if (typeof ErrorUtils === "undefined" || typeof ErrorUtils.setGlobalHandler !== "function") return;
|
|
4006
|
+
const prev = ErrorUtils.getGlobalHandler?.();
|
|
4007
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
4008
|
+
void this.flush();
|
|
4009
|
+
if (typeof prev === "function") prev(error, isFatal);
|
|
4010
|
+
});
|
|
4011
|
+
this.teardowns.push(() => {
|
|
4012
|
+
if (typeof prev === "function") ErrorUtils.setGlobalHandler(prev);
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
/** Flush on background/inactive so timer suspension doesn't strand the tail. */
|
|
4016
|
+
installBackgroundFlush() {
|
|
4017
|
+
const onChange = (state) => {
|
|
4018
|
+
if (state === "background" || state === "inactive") void this.flush();
|
|
4019
|
+
};
|
|
4020
|
+
this.appStateSub = reactNative.AppState.addEventListener("change", onChange);
|
|
4021
|
+
this.teardowns.push(() => {
|
|
4022
|
+
this.appStateSub?.remove();
|
|
4023
|
+
this.appStateSub = null;
|
|
4024
|
+
});
|
|
4119
4025
|
}
|
|
4120
4026
|
/**
|
|
4121
|
-
*
|
|
4122
|
-
*
|
|
4027
|
+
* Acquire the FiberRoot from the provider's root-View public instance (a ref).
|
|
4028
|
+
* Called once on provider mount. This makes capture independent of the React
|
|
4029
|
+
* DevTools commit hook, which Release builds strip — without it the fiber walk
|
|
4030
|
+
* never runs in production and only the initial viewport message is emitted.
|
|
4123
4031
|
*/
|
|
4124
|
-
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
const overlayRemove = win.some((a) => a.overlayRemove);
|
|
4134
|
-
let kind;
|
|
4135
|
-
if (navigated) kind = "navigated";
|
|
4136
|
-
else if (overlayAdd) kind = "opened_overlay";
|
|
4137
|
-
else if (overlayRemove) kind = "closed_overlay";
|
|
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 } };
|
|
4032
|
+
setRootInstance(instance) {
|
|
4033
|
+
const root = fiberRootFromInstance(instance);
|
|
4034
|
+
if (root) {
|
|
4035
|
+
this.explicitRoot = root;
|
|
4036
|
+
if (this.cfg?.debug) console.log("[structural] fiber root acquired via ref");
|
|
4037
|
+
this.sample(true);
|
|
4038
|
+
} else if (this.cfg?.debug) {
|
|
4039
|
+
console.log("[structural] ref did not yield a fiber root \u2014 relying on the commit hook (dev only)");
|
|
4040
|
+
}
|
|
4149
4041
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4042
|
+
sample(force = false) {
|
|
4043
|
+
if (!this.running) return;
|
|
4044
|
+
if (!force && this.overloadSkip > 0) {
|
|
4045
|
+
this.overloadSkip--;
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (!force && !this.explicitRoot && !hookState.dirty) return;
|
|
4049
|
+
hookState.dirty = false;
|
|
4050
|
+
const root = hookState.latestRoot ?? this.explicitRoot;
|
|
4051
|
+
if (!root) return;
|
|
4052
|
+
const t0 = Date.now();
|
|
4053
|
+
try {
|
|
4054
|
+
const { tree, index } = walkFiberRoot(root);
|
|
4055
|
+
this.currentIndex = index;
|
|
4056
|
+
this.recorder.commit(tree, this.now());
|
|
4057
|
+
} catch (e) {
|
|
4058
|
+
if (this.cfg?.debug) console.log("[structural] sample error", String(e));
|
|
4059
|
+
}
|
|
4060
|
+
const cost = Date.now() - t0;
|
|
4061
|
+
if (cost > WALK_BUDGET_MS) {
|
|
4062
|
+
this.overloadSkip = Math.min(MAX_OVERLOAD_SKIP, Math.ceil(cost / WALK_BUDGET_MS));
|
|
4063
|
+
}
|
|
4165
4064
|
}
|
|
4166
|
-
|
|
4167
|
-
this.
|
|
4065
|
+
setScreen(name) {
|
|
4066
|
+
if (!this.running || name === this.screen) return;
|
|
4067
|
+
this.prevScreen = this.screen;
|
|
4068
|
+
this.screen = name;
|
|
4069
|
+
this.sample(true);
|
|
4070
|
+
this.recorder.screenView(name, this.prevScreen, this.now());
|
|
4168
4071
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4072
|
+
/** Resolve a native touch to a node + emit a tap. `target` is the RN
|
|
4073
|
+
* nativeTag from `nativeEvent.target`. */
|
|
4074
|
+
handleTap(target, pageX, pageY) {
|
|
4075
|
+
if (!this.running) return;
|
|
4076
|
+
const entry = this.currentIndex.get(target);
|
|
4077
|
+
if (!entry) return;
|
|
4078
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
4079
|
+
this.recorder.tap(
|
|
4080
|
+
{
|
|
4081
|
+
key: entry.node.key,
|
|
4082
|
+
path: entry.path,
|
|
4083
|
+
attrs: entry.node.attrs,
|
|
4084
|
+
text: entry.node.text,
|
|
4085
|
+
normX: pageX / (width || 1),
|
|
4086
|
+
normY: pageY / (height || 1)
|
|
4087
|
+
},
|
|
4088
|
+
this.now()
|
|
4089
|
+
);
|
|
4172
4090
|
}
|
|
4173
|
-
/** Feed a
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4091
|
+
/** Feed a scroll/drag delta from a touch onto the nearest scrollable ancestor
|
|
4092
|
+
* of the touched node. `target` is the RN nativeTag under the finger; `dx`/`dy`
|
|
4093
|
+
* are content-offset deltas (px, sign = content motion). Native-driven scroll
|
|
4094
|
+
* never reaches the fiber walk, so this is the only way the replay can ride the
|
|
4095
|
+
* scroll instead of freezing on a stale frame. */
|
|
4096
|
+
handleScroll(target, dx, dy) {
|
|
4097
|
+
if (!this.running) return;
|
|
4098
|
+
const leaf = this.currentIndex.get(target);
|
|
4099
|
+
const scrollTag = leaf?.scrollAncestorTag;
|
|
4100
|
+
if (scrollTag === void 0) return;
|
|
4101
|
+
const scrollable = this.currentIndex.get(scrollTag);
|
|
4102
|
+
if (!scrollable) return;
|
|
4103
|
+
const cur = this.scrollOffsets.get(scrollTag) ?? { x: 0, y: 0 };
|
|
4104
|
+
const next = { x: Math.max(0, cur.x + dx), y: Math.max(0, cur.y + dy) };
|
|
4105
|
+
if (Math.abs(next.x - cur.x) < 1 && Math.abs(next.y - cur.y) < 1) return;
|
|
4106
|
+
this.scrollOffsets.set(scrollTag, next);
|
|
4107
|
+
this.recorder.scroll(scrollable.node.key, next.x, next.y, this.now());
|
|
4108
|
+
}
|
|
4109
|
+
/** Public best-effort flush — used by the shared RN error handler to push the
|
|
4110
|
+
* pre-crash tail (the in-class ErrorUtils hook covers apps that don't call
|
|
4111
|
+
* setupRNErrorHandler; both are idempotent via the `flushing` guard). */
|
|
4112
|
+
async flushNow() {
|
|
4113
|
+
await this.flush();
|
|
4114
|
+
}
|
|
4115
|
+
async flush() {
|
|
4116
|
+
if (!this.cfg || this.flushing) return;
|
|
4117
|
+
this.recorder.advance(this.now());
|
|
4118
|
+
const from = this.sentCount;
|
|
4119
|
+
const batch = this.recorder.sliceStream(from);
|
|
4120
|
+
if (batch.length === 0) return;
|
|
4121
|
+
this.flushing = true;
|
|
4122
|
+
const vp = this.recorder.viewportSize;
|
|
4123
|
+
try {
|
|
4124
|
+
const headers = {
|
|
4125
|
+
"content-type": "application/json",
|
|
4126
|
+
"X-API-Key": this.cfg.apiKey
|
|
4127
|
+
};
|
|
4128
|
+
if (this.cfg.projectId) headers["X-Project-ID"] = this.cfg.projectId;
|
|
4129
|
+
const res = await fetch(`${this.cfg.endpoint.replace(/\/$/, "")}/api/replay-stream`, {
|
|
4130
|
+
method: "POST",
|
|
4131
|
+
headers,
|
|
4132
|
+
body: JSON.stringify({
|
|
4133
|
+
session_id: this.sessionId,
|
|
4134
|
+
platform: this.cfg.platform,
|
|
4135
|
+
viewport: { w: vp.w, h: vp.h },
|
|
4136
|
+
seq_start: from,
|
|
4137
|
+
messages: batch
|
|
4138
|
+
})
|
|
4139
|
+
});
|
|
4140
|
+
if (res.ok) {
|
|
4141
|
+
this.sentCount = from + batch.length;
|
|
4142
|
+
if (this.cfg.debug) console.log("[structural] flushed", batch.length, "msgs @", from);
|
|
4143
|
+
} else if (this.cfg.debug) {
|
|
4144
|
+
console.log("[structural] flush rejected", res.status, "\u2014 will retry");
|
|
4145
|
+
}
|
|
4146
|
+
} catch (e) {
|
|
4147
|
+
if (this.cfg.debug) console.log("[structural] flush failed", String(e), "\u2014 buffered");
|
|
4148
|
+
} finally {
|
|
4149
|
+
this.flushing = false;
|
|
4181
4150
|
}
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
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;
|
|
4151
|
+
}
|
|
4152
|
+
async stop() {
|
|
4153
|
+
if (!this.running) return;
|
|
4154
|
+
this.recorder.finalize(this.now());
|
|
4155
|
+
if (this.sampleTimer) clearInterval(this.sampleTimer);
|
|
4156
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
4157
|
+
this.sampleTimer = null;
|
|
4158
|
+
this.flushTimer = null;
|
|
4159
|
+
this.running = false;
|
|
4160
|
+
this.explicitRoot = null;
|
|
4161
|
+
for (const t of this.teardowns.splice(0)) {
|
|
4162
|
+
try {
|
|
4163
|
+
t();
|
|
4164
|
+
} catch {
|
|
4207
4165
|
}
|
|
4208
4166
|
}
|
|
4209
|
-
|
|
4210
|
-
this.analyzer.recordActivity(activity);
|
|
4211
|
-
this.index = index;
|
|
4212
|
-
this.drainPending(t);
|
|
4167
|
+
await this.flush();
|
|
4213
4168
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4169
|
+
};
|
|
4170
|
+
var structuralCapture = new StructuralCapture();
|
|
4171
|
+
|
|
4172
|
+
// src/react-native/error-handler.ts
|
|
4173
|
+
function setupRNErrorHandler(options = {}) {
|
|
4174
|
+
const {
|
|
4175
|
+
captureFatal = true,
|
|
4176
|
+
captureNonFatal = true,
|
|
4177
|
+
capturePromiseRejections = true
|
|
4178
|
+
} = options;
|
|
4179
|
+
const teardowns = [];
|
|
4180
|
+
if (typeof ErrorUtils !== "undefined") {
|
|
4181
|
+
const previousHandler = ErrorUtils.getGlobalHandler();
|
|
4182
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
4183
|
+
if (isFatal && captureFatal || !isFatal && captureNonFatal) {
|
|
4184
|
+
captureError(error, {
|
|
4185
|
+
tags: {
|
|
4186
|
+
fatal: String(isFatal),
|
|
4187
|
+
handler: "react-native-global"
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
4190
|
+
flushScreenRecording().catch(() => {
|
|
4191
|
+
});
|
|
4192
|
+
if (structuralCapture.isRunning()) {
|
|
4193
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
if (previousHandler) {
|
|
4198
|
+
previousHandler(error, isFatal);
|
|
4199
|
+
}
|
|
4200
|
+
});
|
|
4201
|
+
teardowns.push(() => {
|
|
4202
|
+
if (previousHandler) {
|
|
4203
|
+
ErrorUtils.setGlobalHandler(previousHandler);
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
4224
4206
|
}
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4207
|
+
if (capturePromiseRejections) {
|
|
4208
|
+
if (typeof global !== "undefined") {
|
|
4209
|
+
const rejectionHandler = (event) => {
|
|
4210
|
+
const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
4211
|
+
captureError(err, {
|
|
4212
|
+
tags: { handler: "unhandled-promise-rejection" }
|
|
4213
|
+
});
|
|
4214
|
+
flushScreenRecording().catch(() => {
|
|
4215
|
+
});
|
|
4216
|
+
if (structuralCapture.isRunning()) {
|
|
4217
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
if (typeof globalThis.addEventListener === "function") {
|
|
4222
|
+
globalThis.addEventListener("unhandledrejection", rejectionHandler);
|
|
4223
|
+
teardowns.push(() => {
|
|
4224
|
+
globalThis.removeEventListener("unhandledrejection", rejectionHandler);
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4229
4228
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4229
|
+
return () => {
|
|
4230
|
+
for (const teardown of teardowns) {
|
|
4231
|
+
teardown();
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
// src/react-native/navigation.ts
|
|
4237
|
+
function createNavigationTracker(navigationRef, options = {}) {
|
|
4238
|
+
const {
|
|
4239
|
+
trackScreenViews = true,
|
|
4240
|
+
addBreadcrumbs = true,
|
|
4241
|
+
screenNameFilter
|
|
4242
|
+
} = options;
|
|
4243
|
+
let currentRouteName;
|
|
4244
|
+
const unsubscribe = navigationRef.addListener("state", () => {
|
|
4245
|
+
const route = navigationRef.getCurrentRoute();
|
|
4246
|
+
if (!route) return;
|
|
4247
|
+
const previousRouteName = currentRouteName;
|
|
4248
|
+
currentRouteName = route.name;
|
|
4249
|
+
if (previousRouteName === currentRouteName) return;
|
|
4250
|
+
const screenName = screenNameFilter ? screenNameFilter(currentRouteName) : currentRouteName;
|
|
4251
|
+
if (!screenName) return;
|
|
4252
|
+
setCurrentScreen(screenName);
|
|
4253
|
+
if (trackScreenViews) {
|
|
4254
|
+
track("$screen_view", {
|
|
4255
|
+
screen_name: screenName,
|
|
4256
|
+
previous_screen: previousRouteName,
|
|
4257
|
+
params: route.params
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
if (addBreadcrumbs) {
|
|
4261
|
+
addBreadcrumb({
|
|
4262
|
+
type: "navigation",
|
|
4263
|
+
category: "screen",
|
|
4264
|
+
message: `Navigated to ${screenName}`,
|
|
4265
|
+
data: {
|
|
4266
|
+
from: previousRouteName,
|
|
4267
|
+
to: screenName
|
|
4268
|
+
}
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
});
|
|
4272
|
+
return () => {
|
|
4273
|
+
unsubscribe();
|
|
4274
|
+
setCurrentScreen(void 0);
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
// src/react-native/network.ts
|
|
4279
|
+
function setupNetworkInterception(options = {}) {
|
|
4280
|
+
const {
|
|
4281
|
+
addBreadcrumbs = true,
|
|
4282
|
+
ignoreUrls = [/sitepong\.com/],
|
|
4283
|
+
maxDataSize = 1024
|
|
4284
|
+
} = options;
|
|
4285
|
+
if (typeof XMLHttpRequest === "undefined") {
|
|
4286
|
+
return () => {
|
|
4287
|
+
};
|
|
4236
4288
|
}
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4289
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
4290
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
4291
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
4292
|
+
const xhr = this;
|
|
4293
|
+
xhr._sitepong_method = method;
|
|
4294
|
+
xhr._sitepong_url = String(url);
|
|
4295
|
+
return originalOpen.apply(this, [method, url, ...rest]);
|
|
4296
|
+
};
|
|
4297
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
4298
|
+
const xhr = this;
|
|
4299
|
+
const url = xhr._sitepong_url || "";
|
|
4300
|
+
const method = xhr._sitepong_method || "GET";
|
|
4301
|
+
const shouldIgnore = ignoreUrls.some((pattern) => {
|
|
4302
|
+
if (typeof pattern === "string") return url.includes(pattern);
|
|
4303
|
+
return pattern.test(url);
|
|
4304
|
+
});
|
|
4305
|
+
if (shouldIgnore) {
|
|
4306
|
+
return originalSend.call(this, body);
|
|
4242
4307
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4308
|
+
xhr._sitepong_startTime = Date.now();
|
|
4309
|
+
const onLoadEnd = () => {
|
|
4310
|
+
const duration = xhr._sitepong_startTime ? Date.now() - xhr._sitepong_startTime : void 0;
|
|
4311
|
+
if (addBreadcrumbs) {
|
|
4312
|
+
addBreadcrumb({
|
|
4313
|
+
type: "http",
|
|
4314
|
+
category: "xhr",
|
|
4315
|
+
message: `${method} ${url}`,
|
|
4316
|
+
level: xhr.status >= 400 ? "error" : "info",
|
|
4317
|
+
data: {
|
|
4318
|
+
method,
|
|
4319
|
+
url: url.length > maxDataSize ? url.substring(0, maxDataSize) : url,
|
|
4320
|
+
status_code: xhr.status,
|
|
4321
|
+
duration_ms: duration
|
|
4322
|
+
}
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
4325
|
+
};
|
|
4326
|
+
this.addEventListener("loadend", onLoadEnd);
|
|
4327
|
+
return originalSend.call(this, body);
|
|
4328
|
+
};
|
|
4329
|
+
return () => {
|
|
4330
|
+
XMLHttpRequest.prototype.open = originalOpen;
|
|
4331
|
+
XMLHttpRequest.prototype.send = originalSend;
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
var RNAutocaptureModule = class {
|
|
4335
|
+
constructor(options = {}) {
|
|
4336
|
+
this.appStateSubscription = null;
|
|
4337
|
+
this.currentAppState = "active";
|
|
4338
|
+
this.options = {
|
|
4339
|
+
trackAppState: options.trackAppState ?? true
|
|
4340
|
+
};
|
|
4249
4341
|
}
|
|
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);
|
|
4342
|
+
start() {
|
|
4343
|
+
if (this.options.trackAppState) {
|
|
4344
|
+
this.setupAppStateTracking();
|
|
4345
|
+
}
|
|
4258
4346
|
}
|
|
4259
|
-
|
|
4260
|
-
|
|
4347
|
+
stop() {
|
|
4348
|
+
if (this.appStateSubscription) {
|
|
4349
|
+
this.appStateSubscription.remove();
|
|
4350
|
+
this.appStateSubscription = null;
|
|
4351
|
+
}
|
|
4261
4352
|
}
|
|
4262
|
-
|
|
4263
|
-
|
|
4353
|
+
setupAppStateTracking() {
|
|
4354
|
+
try {
|
|
4355
|
+
this.currentAppState = reactNative.AppState.currentState;
|
|
4356
|
+
this.appStateSubscription = reactNative.AppState.addEventListener("change", (nextAppState) => {
|
|
4357
|
+
const previousState = this.currentAppState;
|
|
4358
|
+
this.currentAppState = nextAppState;
|
|
4359
|
+
if (previousState !== nextAppState) {
|
|
4360
|
+
track("$app_state_change", {
|
|
4361
|
+
from: previousState,
|
|
4362
|
+
to: nextAppState
|
|
4363
|
+
});
|
|
4364
|
+
if (previousState.match(/inactive|background/) && nextAppState === "active") {
|
|
4365
|
+
track("$app_foreground", {
|
|
4366
|
+
previous_state: previousState
|
|
4367
|
+
});
|
|
4368
|
+
} else if (previousState === "active" && nextAppState.match(/inactive|background/)) {
|
|
4369
|
+
track("$app_background", {
|
|
4370
|
+
next_state: nextAppState
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
});
|
|
4375
|
+
} catch {
|
|
4376
|
+
}
|
|
4264
4377
|
}
|
|
4265
4378
|
};
|
|
4266
4379
|
|
|
4267
|
-
// src/react-native/
|
|
4268
|
-
var
|
|
4269
|
-
var
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
});
|
|
4273
|
-
function record(root) {
|
|
4274
|
-
hookState.latestRoot = root;
|
|
4275
|
-
hookState.dirty = true;
|
|
4380
|
+
// src/react-native/performance.ts
|
|
4381
|
+
var coldStartTimestamp = null;
|
|
4382
|
+
var coldStartTracked = false;
|
|
4383
|
+
function markColdStart() {
|
|
4384
|
+
coldStartTimestamp = Date.now();
|
|
4276
4385
|
}
|
|
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
|
-
};
|
|
4386
|
+
var RNPerformanceManager = class {
|
|
4387
|
+
constructor() {
|
|
4388
|
+
this.screenRenderStarts = /* @__PURE__ */ new Map();
|
|
4315
4389
|
}
|
|
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) {
|
|
4390
|
+
/**
|
|
4391
|
+
* Track cold start time from markColdStart() to now.
|
|
4392
|
+
* Should be called once the app is interactive (e.g., in the root component's useEffect).
|
|
4393
|
+
*/
|
|
4394
|
+
trackColdStart() {
|
|
4395
|
+
if (coldStartTracked || !coldStartTimestamp) return;
|
|
4396
|
+
const duration = Date.now() - coldStartTimestamp;
|
|
4397
|
+
coldStartTracked = true;
|
|
4398
|
+
track("$cold_start", {
|
|
4399
|
+
duration_ms: duration
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
/**
|
|
4403
|
+
* Start tracking a screen render. Call before rendering starts.
|
|
4404
|
+
*/
|
|
4405
|
+
startScreenRender(screenName) {
|
|
4406
|
+
this.screenRenderStarts.set(screenName, Date.now());
|
|
4407
|
+
}
|
|
4408
|
+
/**
|
|
4409
|
+
* End tracking a screen render. Call after rendering completes.
|
|
4410
|
+
*/
|
|
4411
|
+
endScreenRender(screenName) {
|
|
4412
|
+
const startTime = this.screenRenderStarts.get(screenName);
|
|
4413
|
+
if (startTime === void 0) return null;
|
|
4414
|
+
this.screenRenderStarts.delete(screenName);
|
|
4415
|
+
const duration = Date.now() - startTime;
|
|
4416
|
+
track("$screen_render", {
|
|
4417
|
+
screen_name: screenName,
|
|
4418
|
+
duration_ms: duration
|
|
4419
|
+
});
|
|
4420
|
+
return duration;
|
|
4421
|
+
}
|
|
4422
|
+
/**
|
|
4423
|
+
* Track a screen render using a useEffect-friendly pattern.
|
|
4424
|
+
* Returns a cleanup function.
|
|
4425
|
+
*/
|
|
4426
|
+
trackScreenRender(screenName) {
|
|
4427
|
+
this.startScreenRender(screenName);
|
|
4428
|
+
return () => {
|
|
4429
|
+
this.endScreenRender(screenName);
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
};
|
|
4433
|
+
|
|
4434
|
+
// src/react-native/push.ts
|
|
4435
|
+
var cachedDeviceContext = null;
|
|
4436
|
+
function getEndpoint() {
|
|
4437
|
+
const cfg = sitepong.config;
|
|
4438
|
+
return cfg?.endpoint || "https://ingest.sitepong.com";
|
|
4439
|
+
}
|
|
4440
|
+
function getApiKey() {
|
|
4441
|
+
const cfg = sitepong.config;
|
|
4442
|
+
return cfg?.apiKey || "";
|
|
4443
|
+
}
|
|
4444
|
+
function getDeviceContext() {
|
|
4445
|
+
if (cachedDeviceContext) return cachedDeviceContext;
|
|
4390
4446
|
try {
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4447
|
+
const info = collectDeviceInfo();
|
|
4448
|
+
cachedDeviceContext = {
|
|
4449
|
+
platform: info.platform,
|
|
4450
|
+
appVersion: info.appVersion,
|
|
4451
|
+
deviceModel: info.deviceModel,
|
|
4452
|
+
osVersion: info.osVersion
|
|
4453
|
+
};
|
|
4454
|
+
fetchPersistentDeviceId().then((id) => {
|
|
4455
|
+
if (id && cachedDeviceContext) cachedDeviceContext.deviceId = id;
|
|
4456
|
+
});
|
|
4396
4457
|
} catch {
|
|
4397
|
-
|
|
4458
|
+
cachedDeviceContext = {};
|
|
4398
4459
|
}
|
|
4460
|
+
return cachedDeviceContext;
|
|
4399
4461
|
}
|
|
4400
|
-
function
|
|
4401
|
-
const
|
|
4402
|
-
|
|
4403
|
-
if (
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4462
|
+
async function postToIngest(path, body) {
|
|
4463
|
+
const endpoint = getEndpoint();
|
|
4464
|
+
const apiKey = getApiKey();
|
|
4465
|
+
if (!apiKey) {
|
|
4466
|
+
console.warn("[SitePong] Cannot register push token: SDK not initialized");
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
try {
|
|
4470
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
4471
|
+
method: "POST",
|
|
4472
|
+
headers: {
|
|
4473
|
+
"Content-Type": "application/json",
|
|
4474
|
+
"X-API-Key": apiKey
|
|
4475
|
+
},
|
|
4476
|
+
body: JSON.stringify(body)
|
|
4477
|
+
});
|
|
4478
|
+
if (!response.ok) {
|
|
4479
|
+
console.warn(`[SitePong] Push token registration failed: HTTP ${response.status}`);
|
|
4480
|
+
}
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
console.warn("[SitePong] Push token registration failed:", err);
|
|
4413
4483
|
}
|
|
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
4484
|
}
|
|
4419
|
-
function
|
|
4420
|
-
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4485
|
+
async function deleteFromIngest(path, body) {
|
|
4486
|
+
const endpoint = getEndpoint();
|
|
4487
|
+
const apiKey = getApiKey();
|
|
4488
|
+
if (!apiKey) return;
|
|
4489
|
+
try {
|
|
4490
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
4491
|
+
method: "DELETE",
|
|
4492
|
+
headers: {
|
|
4493
|
+
"Content-Type": "application/json",
|
|
4494
|
+
"X-API-Key": apiKey
|
|
4495
|
+
},
|
|
4496
|
+
body: JSON.stringify(body)
|
|
4497
|
+
});
|
|
4498
|
+
if (!response.ok) {
|
|
4499
|
+
console.warn(`[SitePong] Push token deactivation failed: HTTP ${response.status}`);
|
|
4430
4500
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4501
|
+
} catch (err) {
|
|
4502
|
+
console.warn("[SitePong] Push token deactivation failed:", err);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
function registerPushToken(token, options) {
|
|
4506
|
+
const device = getDeviceContext();
|
|
4507
|
+
postToIngest("/api/push/tokens", {
|
|
4508
|
+
expo_push_token: token,
|
|
4509
|
+
environment: options.environment,
|
|
4510
|
+
device_id: device.deviceId,
|
|
4511
|
+
platform: device.platform,
|
|
4512
|
+
app_version: device.appVersion,
|
|
4513
|
+
device_model: device.deviceModel,
|
|
4514
|
+
os_version: device.osVersion
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
function registerDeviceToken(token, options) {
|
|
4518
|
+
const device = getDeviceContext();
|
|
4519
|
+
const tokenType = options.platform === "ios" ? "apns" : "fcm";
|
|
4520
|
+
postToIngest("/api/push/tokens", {
|
|
4521
|
+
native_device_token: token,
|
|
4522
|
+
token_type: tokenType,
|
|
4523
|
+
environment: options.environment,
|
|
4524
|
+
device_id: device.deviceId,
|
|
4525
|
+
platform: options.platform,
|
|
4526
|
+
app_version: device.appVersion,
|
|
4527
|
+
device_model: device.deviceModel,
|
|
4528
|
+
os_version: device.osVersion
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
function registerLiveActivityToken(activityType, activityId, pushToken, options) {
|
|
4532
|
+
const device = getDeviceContext();
|
|
4533
|
+
postToIngest("/api/push/live-activity-tokens", {
|
|
4534
|
+
activity_type: activityType,
|
|
4535
|
+
activity_id: activityId,
|
|
4536
|
+
push_token: pushToken,
|
|
4537
|
+
environment: options.environment,
|
|
4538
|
+
device_id: device.deviceId
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
function registerPushToStartToken(activityType, pushToStartToken, options) {
|
|
4542
|
+
const device = getDeviceContext();
|
|
4543
|
+
postToIngest("/api/push/push-to-start-tokens", {
|
|
4544
|
+
activity_type: activityType,
|
|
4545
|
+
push_to_start_token: pushToStartToken,
|
|
4546
|
+
environment: options.environment,
|
|
4547
|
+
device_id: device.deviceId
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
function endLiveActivity(activityType, activityId) {
|
|
4551
|
+
deleteFromIngest("/api/push/live-activity-tokens", {
|
|
4552
|
+
activity_type: activityType,
|
|
4553
|
+
activity_id: activityId
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
var SitePongRNContext = React2.createContext({
|
|
4557
|
+
isInitialized: false,
|
|
4558
|
+
performanceManager: null
|
|
4559
|
+
});
|
|
4560
|
+
function SitePongRNProvider({
|
|
4561
|
+
apiKey,
|
|
4562
|
+
config = {},
|
|
4563
|
+
asyncStorage,
|
|
4564
|
+
navigationRef,
|
|
4565
|
+
trackNetwork = true,
|
|
4566
|
+
trackAppState = true,
|
|
4567
|
+
captureErrors = true,
|
|
4568
|
+
screenRecording,
|
|
4569
|
+
devicePushToken,
|
|
4570
|
+
devicePlatform,
|
|
4571
|
+
pushEnvironment = "production",
|
|
4572
|
+
pushUserId,
|
|
4573
|
+
children
|
|
4574
|
+
}) {
|
|
4575
|
+
const initialized = React2.useRef(false);
|
|
4576
|
+
const [sdkReady, setSdkReady] = React2.useState(false);
|
|
4577
|
+
const performanceManagerRef = React2.useRef(null);
|
|
4578
|
+
React2.useEffect(() => {
|
|
4579
|
+
if (initialized.current) return;
|
|
4580
|
+
const teardowns = [];
|
|
4581
|
+
const deviceInfo = collectDeviceInfo();
|
|
4582
|
+
setRNDeviceInfo({
|
|
4583
|
+
platform: "react-native",
|
|
4584
|
+
osName: deviceInfo.platform,
|
|
4585
|
+
osVersion: deviceInfo.osVersion,
|
|
4586
|
+
deviceName: deviceInfo.deviceName,
|
|
4587
|
+
deviceBrand: deviceInfo.deviceBrand,
|
|
4588
|
+
deviceModel: deviceInfo.deviceModel,
|
|
4589
|
+
appVersion: deviceInfo.appVersion,
|
|
4590
|
+
screenWidth: deviceInfo.screenWidth,
|
|
4591
|
+
screenHeight: deviceInfo.screenHeight,
|
|
4592
|
+
isEmulator: deviceInfo.isEmulator
|
|
4593
|
+
});
|
|
4594
|
+
fetchPersistentDeviceId().then((id) => {
|
|
4595
|
+
if (id) setNativeDeviceId(id);
|
|
4596
|
+
}).catch(() => {
|
|
4597
|
+
});
|
|
4598
|
+
const storageAdapter = asyncStorage ? createAsyncStorageAdapter(asyncStorage) : void 0;
|
|
4599
|
+
initRN({
|
|
4600
|
+
apiKey,
|
|
4601
|
+
...config,
|
|
4602
|
+
_storageAdapter: storageAdapter
|
|
4603
|
+
});
|
|
4604
|
+
if (captureErrors) {
|
|
4605
|
+
const teardownErrors = setupRNErrorHandler();
|
|
4606
|
+
teardowns.push(teardownErrors);
|
|
4607
|
+
}
|
|
4608
|
+
if (trackNetwork) {
|
|
4609
|
+
const teardownNetwork = setupNetworkInterception();
|
|
4610
|
+
teardowns.push(teardownNetwork);
|
|
4611
|
+
}
|
|
4612
|
+
if (trackAppState) {
|
|
4613
|
+
const autocapture = new RNAutocaptureModule({ trackAppState: true });
|
|
4614
|
+
autocapture.start();
|
|
4615
|
+
teardowns.push(() => autocapture.stop());
|
|
4616
|
+
}
|
|
4617
|
+
if (navigationRef) {
|
|
4618
|
+
const teardownNav = createNavigationTracker(navigationRef);
|
|
4619
|
+
teardowns.push(teardownNav);
|
|
4620
|
+
}
|
|
4621
|
+
if (screenRecording?.enabled) {
|
|
4622
|
+
const sessionId = `rn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
4623
|
+
const endpoint = config.endpoint || "https://ingest.sitepong.com";
|
|
4624
|
+
startScreenRecordingOnError(apiKey, endpoint, sessionId, screenRecording);
|
|
4625
|
+
teardowns.push(() => stopScreenRecording());
|
|
4626
|
+
}
|
|
4627
|
+
if (devicePushToken && devicePlatform) {
|
|
4628
|
+
registerDeviceToken(devicePushToken, { platform: devicePlatform, environment: pushEnvironment});
|
|
4629
|
+
}
|
|
4630
|
+
performanceManagerRef.current = new RNPerformanceManager();
|
|
4631
|
+
performanceManagerRef.current.trackColdStart();
|
|
4632
|
+
initialized.current = true;
|
|
4633
|
+
setSdkReady(true);
|
|
4634
|
+
return () => {
|
|
4635
|
+
for (const teardown of teardowns) {
|
|
4636
|
+
teardown();
|
|
4637
|
+
}
|
|
4638
|
+
};
|
|
4639
|
+
}, [apiKey]);
|
|
4640
|
+
const value = {
|
|
4641
|
+
isInitialized: sdkReady,
|
|
4642
|
+
performanceManager: performanceManagerRef.current
|
|
4433
4643
|
};
|
|
4434
|
-
|
|
4435
|
-
return out;
|
|
4644
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SitePongRNContext.Provider, { value, children });
|
|
4436
4645
|
}
|
|
4437
|
-
function
|
|
4438
|
-
|
|
4439
|
-
let f = fiber;
|
|
4440
|
-
while (f) {
|
|
4441
|
-
collectFrom(f, inheritedName, out, index, path);
|
|
4442
|
-
f = f.sibling;
|
|
4443
|
-
}
|
|
4444
|
-
return out;
|
|
4646
|
+
function useSitePongRNContext() {
|
|
4647
|
+
return React2.useContext(SitePongRNContext);
|
|
4445
4648
|
}
|
|
4446
|
-
function
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4649
|
+
function useScreenTrack(screenName) {
|
|
4650
|
+
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
4651
|
+
React2.useEffect(() => {
|
|
4652
|
+
if (!performanceManager) return;
|
|
4653
|
+
performanceManager.startScreenRender(screenName);
|
|
4654
|
+
return () => {
|
|
4655
|
+
performanceManager.endScreenRender(screenName);
|
|
4656
|
+
};
|
|
4657
|
+
}, [screenName, performanceManager]);
|
|
4451
4658
|
}
|
|
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
|
-
}
|
|
4659
|
+
function useAppState() {
|
|
4660
|
+
const [appState, setAppState] = React2.useState("active");
|
|
4661
|
+
React2.useEffect(() => {
|
|
4662
|
+
setAppState(reactNative.AppState.currentState);
|
|
4663
|
+
const subscription = reactNative.AppState.addEventListener("change", (nextState) => {
|
|
4664
|
+
setAppState(nextState);
|
|
4665
|
+
});
|
|
4666
|
+
return () => subscription.remove();
|
|
4667
|
+
}, []);
|
|
4668
|
+
return appState;
|
|
4476
4669
|
}
|
|
4477
|
-
function
|
|
4478
|
-
const
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4670
|
+
function useRemoteConfig() {
|
|
4671
|
+
const [config, setConfig] = React2.useState(() => getRemoteConfig());
|
|
4672
|
+
React2.useEffect(() => {
|
|
4673
|
+
setConfig(getRemoteConfig());
|
|
4674
|
+
const unsubscribe = onRemoteConfigChange((newConfig) => {
|
|
4675
|
+
setConfig(newConfig);
|
|
4676
|
+
});
|
|
4677
|
+
return unsubscribe;
|
|
4678
|
+
}, []);
|
|
4679
|
+
return config;
|
|
4485
4680
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
const
|
|
4490
|
-
|
|
4681
|
+
function useRNPerformance(screenName) {
|
|
4682
|
+
const { performanceManager } = React2.useContext(SitePongRNContext);
|
|
4683
|
+
const [duration, setDuration] = React2.useState(null);
|
|
4684
|
+
const startTimeRef = React2.useRef(Date.now());
|
|
4685
|
+
React2.useEffect(() => {
|
|
4686
|
+
startTimeRef.current = Date.now();
|
|
4687
|
+
const handle = requestAnimationFrame(() => {
|
|
4688
|
+
const d = Date.now() - startTimeRef.current;
|
|
4689
|
+
setDuration(d);
|
|
4690
|
+
if (performanceManager) {
|
|
4691
|
+
performanceManager.startScreenRender(screenName);
|
|
4692
|
+
performanceManager.endScreenRender(screenName);
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
return () => cancelAnimationFrame(handle);
|
|
4696
|
+
}, [screenName, performanceManager]);
|
|
4697
|
+
return { duration };
|
|
4491
4698
|
}
|
|
4492
|
-
var
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
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));
|
|
4699
|
+
var warnedMissing = false;
|
|
4700
|
+
function loadBridge() {
|
|
4701
|
+
return getScreenRecorderModule();
|
|
4702
|
+
}
|
|
4703
|
+
var started = false;
|
|
4704
|
+
function startWatchtower(config) {
|
|
4705
|
+
if (started) return;
|
|
4706
|
+
const bridge = loadBridge();
|
|
4707
|
+
if (!bridge) {
|
|
4708
|
+
if (!warnedMissing) {
|
|
4709
|
+
warnedMissing = true;
|
|
4710
|
+
console.warn(
|
|
4711
|
+
"[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."
|
|
4712
|
+
);
|
|
4540
4713
|
}
|
|
4714
|
+
return;
|
|
4541
4715
|
}
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4716
|
+
bridge.watchtowerStart({
|
|
4717
|
+
apiKey: config.apiKey,
|
|
4718
|
+
projectId: config.projectId,
|
|
4719
|
+
endpoint: config.endpoint,
|
|
4720
|
+
sampleRate: config.sampleRate ?? 0.1,
|
|
4721
|
+
platform: config.platform ?? "react-native-ios"
|
|
4722
|
+
});
|
|
4723
|
+
started = true;
|
|
4724
|
+
if (config.distinctId) setWatchtowerUser(config.distinctId);
|
|
4725
|
+
}
|
|
4726
|
+
function setWatchtowerUser(distinctId) {
|
|
4727
|
+
try {
|
|
4728
|
+
loadBridge()?.watchtowerSetUser(distinctId);
|
|
4729
|
+
} catch {
|
|
4548
4730
|
}
|
|
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
|
-
);
|
|
4731
|
+
}
|
|
4732
|
+
function getWatchtowerSessionId() {
|
|
4733
|
+
try {
|
|
4734
|
+
return loadBridge()?.watchtowerGetSessionId() ?? null;
|
|
4735
|
+
} catch {
|
|
4736
|
+
return null;
|
|
4567
4737
|
}
|
|
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));
|
|
4738
|
+
}
|
|
4739
|
+
function stopWatchtower() {
|
|
4740
|
+
if (!started) return;
|
|
4741
|
+
loadBridge()?.watchtowerStop();
|
|
4742
|
+
started = false;
|
|
4743
|
+
}
|
|
4744
|
+
function subscribeNavigationToWatchtower(navigationRef) {
|
|
4745
|
+
const unsubscribe = createNavigationTracker(navigationRef, {
|
|
4746
|
+
screenNameFilter: (name) => {
|
|
4747
|
+
try {
|
|
4748
|
+
loadBridge()?.watchtowerSetScreen(name);
|
|
4749
|
+
} catch {
|
|
4750
|
+
}
|
|
4751
|
+
return name;
|
|
4593
4752
|
}
|
|
4753
|
+
});
|
|
4754
|
+
try {
|
|
4755
|
+
const route = navigationRef.getCurrentRoute?.();
|
|
4756
|
+
if (route?.name) loadBridge()?.watchtowerSetScreen(route.name);
|
|
4757
|
+
} catch {
|
|
4594
4758
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
};
|
|
4606
|
-
|
|
4759
|
+
return unsubscribe;
|
|
4760
|
+
}
|
|
4761
|
+
function WatchtowerProvider(props) {
|
|
4762
|
+
const { config, navigationRef, children } = props;
|
|
4763
|
+
React2__default.default.useEffect(() => {
|
|
4764
|
+
startWatchtower(config);
|
|
4765
|
+
const unsubscribe = subscribeNavigationToWatchtower(navigationRef);
|
|
4766
|
+
return () => {
|
|
4767
|
+
unsubscribe();
|
|
4768
|
+
stopWatchtower();
|
|
4769
|
+
};
|
|
4770
|
+
}, []);
|
|
4771
|
+
return React2__default.default.createElement(React2__default.default.Fragment, null, children);
|
|
4772
|
+
}
|
|
4607
4773
|
var TAP_MOVE_THRESHOLD_PX = 12;
|
|
4608
4774
|
function StructuralCaptureProvider({ children, ...cfg }) {
|
|
4775
|
+
const cfgRef = React2.useRef(cfg);
|
|
4776
|
+
cfgRef.current = cfg;
|
|
4777
|
+
const rootRef = React2.useRef(null);
|
|
4609
4778
|
React2.useEffect(() => {
|
|
4610
|
-
structuralCapture.start(
|
|
4779
|
+
structuralCapture.start(cfgRef.current);
|
|
4780
|
+
structuralCapture.setRootInstance(rootRef.current);
|
|
4611
4781
|
return () => {
|
|
4612
4782
|
void structuralCapture.stop();
|
|
4613
4783
|
};
|
|
4614
4784
|
}, []);
|
|
4615
|
-
const
|
|
4785
|
+
const gesture = React2.useRef(null);
|
|
4616
4786
|
const onTouchStart = (e) => {
|
|
4617
|
-
const
|
|
4618
|
-
|
|
4787
|
+
const ne = e.nativeEvent;
|
|
4788
|
+
gesture.current = {
|
|
4789
|
+
startX: ne.pageX,
|
|
4790
|
+
startY: ne.pageY,
|
|
4791
|
+
lastX: ne.pageX,
|
|
4792
|
+
lastY: ne.pageY,
|
|
4793
|
+
target: Number(ne.target),
|
|
4794
|
+
scrolling: false
|
|
4795
|
+
};
|
|
4796
|
+
};
|
|
4797
|
+
const onTouchMove = (e) => {
|
|
4798
|
+
const g2 = gesture.current;
|
|
4799
|
+
if (!g2) return;
|
|
4800
|
+
const ne = e.nativeEvent;
|
|
4801
|
+
const movedFar = Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX;
|
|
4802
|
+
if (movedFar) g2.scrolling = true;
|
|
4803
|
+
if (g2.scrolling) {
|
|
4804
|
+
const dx = g2.lastX - ne.pageX;
|
|
4805
|
+
const dy = g2.lastY - ne.pageY;
|
|
4806
|
+
structuralCapture.handleScroll(g2.target, dx, dy);
|
|
4807
|
+
}
|
|
4808
|
+
g2.lastX = ne.pageX;
|
|
4809
|
+
g2.lastY = ne.pageY;
|
|
4619
4810
|
};
|
|
4620
4811
|
const onTouchEnd = (e) => {
|
|
4812
|
+
const g2 = gesture.current;
|
|
4813
|
+
gesture.current = null;
|
|
4814
|
+
if (!g2) return;
|
|
4815
|
+
if (g2.scrolling) return;
|
|
4621
4816
|
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) {
|
|
4817
|
+
if (Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX) {
|
|
4626
4818
|
return;
|
|
4627
4819
|
}
|
|
4628
|
-
structuralCapture.handleTap(
|
|
4820
|
+
structuralCapture.handleTap(g2.target, ne.pageX, ne.pageY);
|
|
4629
4821
|
};
|
|
4630
|
-
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, onTouchStart, onTouchEnd, children });
|
|
4822
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: rootRef, style: { flex: 1 }, onTouchStart, onTouchMove, onTouchEnd, children });
|
|
4631
4823
|
}
|
|
4632
4824
|
function useStructuralScreen(name) {
|
|
4633
4825
|
React2.useEffect(() => {
|