sitepong 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/entries/rn.js
CHANGED
|
@@ -9,12 +9,15 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
9
9
|
|
|
10
10
|
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
11
11
|
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
14
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
13
15
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
14
16
|
}) : x)(function(x) {
|
|
15
17
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
16
18
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
17
19
|
});
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
18
21
|
|
|
19
22
|
// src/platforms/rn/environment.ts
|
|
20
23
|
function createRNEnvironment(options = {}) {
|
|
@@ -42,8 +45,8 @@ function createRNEnvironment(options = {}) {
|
|
|
42
45
|
return `SitePong-RN/${[osPart, devicePart].filter(Boolean).join("; ")}`;
|
|
43
46
|
},
|
|
44
47
|
onUncaughtError(handler) {
|
|
45
|
-
const
|
|
46
|
-
const ErrorUtils2 =
|
|
48
|
+
const g2 = globalThis;
|
|
49
|
+
const ErrorUtils2 = g2.ErrorUtils;
|
|
47
50
|
if (!ErrorUtils2 || !ErrorUtils2.setGlobalHandler || !ErrorUtils2.getGlobalHandler) {
|
|
48
51
|
return () => {
|
|
49
52
|
};
|
|
@@ -95,10 +98,10 @@ function getEnvironment() {
|
|
|
95
98
|
}
|
|
96
99
|
function detectPlatform() {
|
|
97
100
|
if (currentEnv) return currentEnv.platform;
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
101
|
-
if (
|
|
101
|
+
const g2 = globalThis;
|
|
102
|
+
if (g2.navigator && g2.navigator.product === "ReactNative") return "react-native";
|
|
103
|
+
if (g2.process && g2.process.versions && g2.process.versions.node) return "node";
|
|
104
|
+
if (g2.window && g2.document) return "browser";
|
|
102
105
|
return "unknown";
|
|
103
106
|
}
|
|
104
107
|
function isReactNative() {
|
|
@@ -2138,98 +2141,98 @@ function extractIdentityMetadata(url) {
|
|
|
2138
2141
|
// src/index.ts
|
|
2139
2142
|
function installFallbackEnvironment() {
|
|
2140
2143
|
if (getEnvironment()) return;
|
|
2141
|
-
const
|
|
2142
|
-
const hasBrowser = typeof
|
|
2143
|
-
const hasNode = !hasBrowser && !!
|
|
2144
|
+
const g2 = globalThis;
|
|
2145
|
+
const hasBrowser = typeof g2.window !== "undefined" && typeof g2.document !== "undefined";
|
|
2146
|
+
const hasNode = !hasBrowser && !!g2.process?.versions?.node;
|
|
2144
2147
|
const platform2 = hasBrowser ? "browser" : "node";
|
|
2145
2148
|
const env2 = {
|
|
2146
2149
|
platform: platform2,
|
|
2147
2150
|
storage: null,
|
|
2148
2151
|
getDeviceInfo() {
|
|
2149
2152
|
const info = { platform: platform2 };
|
|
2150
|
-
if (
|
|
2151
|
-
if (hasBrowser &&
|
|
2152
|
-
info.screenWidth =
|
|
2153
|
-
info.screenHeight =
|
|
2153
|
+
if (g2.navigator?.userAgent) info.userAgent = g2.navigator.userAgent;
|
|
2154
|
+
if (hasBrowser && g2.window?.screen) {
|
|
2155
|
+
info.screenWidth = g2.window.screen.width;
|
|
2156
|
+
info.screenHeight = g2.window.screen.height;
|
|
2154
2157
|
}
|
|
2155
|
-
if (hasNode &&
|
|
2156
|
-
info.osName =
|
|
2157
|
-
info.osVersion =
|
|
2158
|
+
if (hasNode && g2.process) {
|
|
2159
|
+
info.osName = g2.process.platform;
|
|
2160
|
+
info.osVersion = g2.process.versions?.node;
|
|
2158
2161
|
}
|
|
2159
2162
|
return info;
|
|
2160
2163
|
},
|
|
2161
2164
|
getCurrentUrl() {
|
|
2162
|
-
return
|
|
2165
|
+
return g2.window?.location?.href ?? void 0;
|
|
2163
2166
|
},
|
|
2164
2167
|
getReferrer() {
|
|
2165
|
-
return
|
|
2168
|
+
return g2.document?.referrer || void 0;
|
|
2166
2169
|
},
|
|
2167
2170
|
getUserAgent() {
|
|
2168
|
-
if (
|
|
2169
|
-
if (hasNode &&
|
|
2171
|
+
if (g2.navigator?.userAgent) return g2.navigator.userAgent;
|
|
2172
|
+
if (hasNode && g2.process) return `SitePong-Node/${g2.process.versions?.node} (${g2.process.platform})`;
|
|
2170
2173
|
return void 0;
|
|
2171
2174
|
},
|
|
2172
2175
|
onUncaughtError(handler) {
|
|
2173
|
-
if (hasBrowser &&
|
|
2176
|
+
if (hasBrowser && g2.window?.addEventListener) {
|
|
2174
2177
|
const listener = (event) => handler(event.error || event.message);
|
|
2175
|
-
|
|
2176
|
-
return () =>
|
|
2178
|
+
g2.window.addEventListener("error", listener);
|
|
2179
|
+
return () => g2.window?.removeEventListener?.("error", listener);
|
|
2177
2180
|
}
|
|
2178
|
-
if (hasNode &&
|
|
2181
|
+
if (hasNode && g2.process?.on && g2.process.off) {
|
|
2179
2182
|
const listener = (err) => handler(err);
|
|
2180
|
-
|
|
2181
|
-
return () =>
|
|
2183
|
+
g2.process.on("uncaughtException", listener);
|
|
2184
|
+
return () => g2.process?.off?.("uncaughtException", listener);
|
|
2182
2185
|
}
|
|
2183
2186
|
return () => {
|
|
2184
2187
|
};
|
|
2185
2188
|
},
|
|
2186
2189
|
onUnhandledRejection(handler) {
|
|
2187
|
-
if (hasBrowser &&
|
|
2190
|
+
if (hasBrowser && g2.window?.addEventListener) {
|
|
2188
2191
|
const listener = (event) => handler(event.reason);
|
|
2189
|
-
|
|
2190
|
-
return () =>
|
|
2192
|
+
g2.window.addEventListener("unhandledrejection", listener);
|
|
2193
|
+
return () => g2.window?.removeEventListener?.("unhandledrejection", listener);
|
|
2191
2194
|
}
|
|
2192
|
-
if (hasNode &&
|
|
2195
|
+
if (hasNode && g2.process?.on && g2.process.off) {
|
|
2193
2196
|
const listener = (reason) => handler(reason);
|
|
2194
|
-
|
|
2195
|
-
return () =>
|
|
2197
|
+
g2.process.on("unhandledRejection", listener);
|
|
2198
|
+
return () => g2.process?.off?.("unhandledRejection", listener);
|
|
2196
2199
|
}
|
|
2197
2200
|
return () => {
|
|
2198
2201
|
};
|
|
2199
2202
|
},
|
|
2200
2203
|
onBeforeTerminate(handler) {
|
|
2201
|
-
if (hasBrowser &&
|
|
2202
|
-
|
|
2203
|
-
return () =>
|
|
2204
|
+
if (hasBrowser && g2.window?.addEventListener) {
|
|
2205
|
+
g2.window.addEventListener("beforeunload", handler);
|
|
2206
|
+
return () => g2.window?.removeEventListener?.("beforeunload", handler);
|
|
2204
2207
|
}
|
|
2205
|
-
if (hasNode &&
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2208
|
+
if (hasNode && g2.process?.on && g2.process.off) {
|
|
2209
|
+
g2.process.on("beforeExit", handler);
|
|
2210
|
+
g2.process.on("SIGINT", handler);
|
|
2211
|
+
g2.process.on("SIGTERM", handler);
|
|
2209
2212
|
return () => {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
+
g2.process?.off?.("beforeExit", handler);
|
|
2214
|
+
g2.process?.off?.("SIGINT", handler);
|
|
2215
|
+
g2.process?.off?.("SIGTERM", handler);
|
|
2213
2216
|
};
|
|
2214
2217
|
}
|
|
2215
2218
|
return () => {
|
|
2216
2219
|
};
|
|
2217
2220
|
},
|
|
2218
2221
|
onVisibilityHidden(handler) {
|
|
2219
|
-
if (
|
|
2222
|
+
if (g2.document?.addEventListener) {
|
|
2220
2223
|
const listener = () => {
|
|
2221
|
-
if (
|
|
2224
|
+
if (g2.document?.visibilityState === "hidden") handler();
|
|
2222
2225
|
};
|
|
2223
|
-
|
|
2224
|
-
return () =>
|
|
2226
|
+
g2.document.addEventListener("visibilitychange", listener);
|
|
2227
|
+
return () => g2.document?.removeEventListener?.("visibilitychange", listener);
|
|
2225
2228
|
}
|
|
2226
2229
|
return () => {
|
|
2227
2230
|
};
|
|
2228
2231
|
},
|
|
2229
2232
|
sendBeacon(url, body) {
|
|
2230
|
-
if (typeof
|
|
2233
|
+
if (typeof g2.navigator?.sendBeacon !== "function") return false;
|
|
2231
2234
|
try {
|
|
2232
|
-
return
|
|
2235
|
+
return g2.navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
2233
2236
|
} catch {
|
|
2234
2237
|
return false;
|
|
2235
2238
|
}
|
|
@@ -3312,6 +3315,824 @@ function SensitiveView(props) {
|
|
|
3312
3315
|
);
|
|
3313
3316
|
}
|
|
3314
3317
|
|
|
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));
|
|
3363
|
+
};
|
|
3364
|
+
if (root) rec(root, null, 0);
|
|
3365
|
+
}
|
|
3366
|
+
var Mirror = class {
|
|
3367
|
+
constructor() {
|
|
3368
|
+
__publicField(this, "ids", /* @__PURE__ */ new WeakMap());
|
|
3369
|
+
__publicField(this, "byId", /* @__PURE__ */ new Map());
|
|
3370
|
+
__publicField(this, "next", 1);
|
|
3371
|
+
}
|
|
3372
|
+
// 0 is reserved for "no/root parent"
|
|
3373
|
+
/** Existing id for a node, or undefined if never registered. */
|
|
3374
|
+
peek(key) {
|
|
3375
|
+
return this.ids.get(key);
|
|
3376
|
+
}
|
|
3377
|
+
/** Id for a node, assigning a fresh one on first sight. Second tuple element
|
|
3378
|
+
* is true when the id was just created (→ emit a Create* message). */
|
|
3379
|
+
getId(key) {
|
|
3380
|
+
const existing = this.ids.get(key);
|
|
3381
|
+
if (existing !== void 0) return [existing, false];
|
|
3382
|
+
const id = this.next++;
|
|
3383
|
+
this.ids.set(key, id);
|
|
3384
|
+
this.byId.set(id, key);
|
|
3385
|
+
return [id, true];
|
|
3386
|
+
}
|
|
3387
|
+
getKey(id) {
|
|
3388
|
+
return this.byId.get(id);
|
|
3389
|
+
}
|
|
3390
|
+
/** Drop a node's id (on unmount). Idempotent. */
|
|
3391
|
+
release(key) {
|
|
3392
|
+
const id = this.ids.get(key);
|
|
3393
|
+
if (id === void 0) return void 0;
|
|
3394
|
+
this.ids.delete(key);
|
|
3395
|
+
this.byId.delete(id);
|
|
3396
|
+
return id;
|
|
3397
|
+
}
|
|
3398
|
+
get size() {
|
|
3399
|
+
return this.byId.size;
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
var emptyIndex = () => /* @__PURE__ */ new Map();
|
|
3403
|
+
function indexTree(tree, mirror, prev) {
|
|
3404
|
+
const next = /* @__PURE__ */ new Map();
|
|
3405
|
+
const order = [];
|
|
3406
|
+
walk(tree, (node, parent, idx) => {
|
|
3407
|
+
const [id] = mirror.getId(node.key);
|
|
3408
|
+
const parentId = parent ? mirror.getId(parent.key)[0] : 0;
|
|
3409
|
+
next.set(id, {
|
|
3410
|
+
id,
|
|
3411
|
+
parentId,
|
|
3412
|
+
index: idx,
|
|
3413
|
+
tag: node.tag,
|
|
3414
|
+
attrs: node.attrs,
|
|
3415
|
+
text: node.text
|
|
3416
|
+
});
|
|
3417
|
+
order.push(id);
|
|
3418
|
+
});
|
|
3419
|
+
return { next, order };
|
|
3420
|
+
}
|
|
3421
|
+
function attrDiff(id, prev, next) {
|
|
3422
|
+
const out = [];
|
|
3423
|
+
for (const k of Object.keys(next)) {
|
|
3424
|
+
if (prev[k] !== next[k]) out.push(SetNodeAttribute(id, k, next[k]));
|
|
3425
|
+
}
|
|
3426
|
+
for (const k of Object.keys(prev)) {
|
|
3427
|
+
if (!(k in next)) out.push(RemoveNodeAttribute(id, k));
|
|
3428
|
+
}
|
|
3429
|
+
return out;
|
|
3430
|
+
}
|
|
3431
|
+
function diff(prev, next, mirror) {
|
|
3432
|
+
const { next: nextIndex, order } = indexTree(next, mirror);
|
|
3433
|
+
const messages = [];
|
|
3434
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3435
|
+
for (const id of prev.keys()) if (!nextIndex.has(id)) removed.add(id);
|
|
3436
|
+
for (const id of removed) {
|
|
3437
|
+
const rec = prev.get(id);
|
|
3438
|
+
if (!removed.has(rec.parentId)) messages.push(RemoveNode(id));
|
|
3439
|
+
}
|
|
3440
|
+
for (const id of order) {
|
|
3441
|
+
const rec = nextIndex.get(id);
|
|
3442
|
+
const before = prev.get(id);
|
|
3443
|
+
if (!before) {
|
|
3444
|
+
if (rec.tag === "#text") {
|
|
3445
|
+
messages.push(CreateTextNode(id, rec.parentId, rec.index));
|
|
3446
|
+
} else {
|
|
3447
|
+
messages.push(CreateElementNode(id, rec.parentId, rec.index, rec.tag));
|
|
3448
|
+
}
|
|
3449
|
+
for (const k of Object.keys(rec.attrs)) messages.push(SetNodeAttribute(id, k, rec.attrs[k]));
|
|
3450
|
+
if (rec.text !== void 0 && rec.text !== "") messages.push(SetNodeData(id, rec.text));
|
|
3451
|
+
} else {
|
|
3452
|
+
if (before.parentId !== rec.parentId || before.index !== rec.index) {
|
|
3453
|
+
messages.push(MoveNode(id, rec.parentId, rec.index));
|
|
3454
|
+
}
|
|
3455
|
+
messages.push(...attrDiff(id, before.attrs, rec.attrs));
|
|
3456
|
+
if ((before.text ?? "") !== (rec.text ?? "")) messages.push(SetNodeData(id, rec.text ?? ""));
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return { messages, index: nextIndex };
|
|
3460
|
+
}
|
|
3461
|
+
function stableAnchor(step) {
|
|
3462
|
+
if (step.attrs.testID) return `@${step.attrs.testID}`;
|
|
3463
|
+
if (step.attrs.nativeID) return `#${step.attrs.nativeID}`;
|
|
3464
|
+
return null;
|
|
3465
|
+
}
|
|
3466
|
+
function computeSelector(path) {
|
|
3467
|
+
if (path.length === 0) return "";
|
|
3468
|
+
const self = path[path.length - 1];
|
|
3469
|
+
const own = stableAnchor(self);
|
|
3470
|
+
if (own) return own;
|
|
3471
|
+
const parts = [];
|
|
3472
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
3473
|
+
const step = path[i];
|
|
3474
|
+
const anchor = stableAnchor(step);
|
|
3475
|
+
if (anchor && i !== path.length - 1) {
|
|
3476
|
+
parts.unshift(anchor);
|
|
3477
|
+
return parts.join(" > ");
|
|
3478
|
+
}
|
|
3479
|
+
const a11y = step.attrs.accessibilityLabel;
|
|
3480
|
+
const comp = step.attrs.rnComponent;
|
|
3481
|
+
if (i === path.length - 1 && a11y) {
|
|
3482
|
+
parts.unshift(`${step.tag}[a11y="${truncate(a11y)}"]`);
|
|
3483
|
+
} else if (comp && comp !== step.tag) {
|
|
3484
|
+
parts.unshift(`${comp}`);
|
|
3485
|
+
} else {
|
|
3486
|
+
parts.unshift(`${step.tag}:nth-of-type(${step.nthOfType})`);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
return parts.join(" > ");
|
|
3490
|
+
}
|
|
3491
|
+
function truncate(s, n = 40) {
|
|
3492
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
3493
|
+
}
|
|
3494
|
+
function computeLabel(attrs, text) {
|
|
3495
|
+
if (attrs.accessibilityLabel) return truncate(attrs.accessibilityLabel, 100);
|
|
3496
|
+
if (attrs.testID) return attrs.testID;
|
|
3497
|
+
if (text && text.trim()) return truncate(text.trim(), 100);
|
|
3498
|
+
if (attrs.rnComponent) return attrs.rnComponent;
|
|
3499
|
+
return "";
|
|
3500
|
+
}
|
|
3501
|
+
var RAGE_WINDOW_MS = 800;
|
|
3502
|
+
var RAGE_MIN_COUNT = 3;
|
|
3503
|
+
var OUTCOME_WINDOW_MS = 1e3;
|
|
3504
|
+
var TOGGLE_ATTR_MAX = 4;
|
|
3505
|
+
var OUTCOME_LABELS = {
|
|
3506
|
+
navigated: "Navigated to a new screen",
|
|
3507
|
+
opened_overlay: "Opened a modal / overlay",
|
|
3508
|
+
closed_overlay: "Dismissed a modal / overlay",
|
|
3509
|
+
toggled: "Toggled UI state",
|
|
3510
|
+
content_changed: "Updated on-screen content",
|
|
3511
|
+
no_effect: "Nothing changed (dead tap)"
|
|
3512
|
+
};
|
|
3513
|
+
var InteractionAnalyzer = class {
|
|
3514
|
+
constructor() {
|
|
3515
|
+
__publicField(this, "activity", []);
|
|
3516
|
+
__publicField(this, "screenViews", []);
|
|
3517
|
+
__publicField(this, "taps", []);
|
|
3518
|
+
}
|
|
3519
|
+
recordActivity(a) {
|
|
3520
|
+
this.activity.push(a);
|
|
3521
|
+
}
|
|
3522
|
+
recordScreenView(t) {
|
|
3523
|
+
this.screenViews.push(t);
|
|
3524
|
+
}
|
|
3525
|
+
/** Register a tap; returns whether it is (so far) a rage tap and the count. */
|
|
3526
|
+
recordTap(tap) {
|
|
3527
|
+
this.taps.push(tap);
|
|
3528
|
+
const from = tap.t - RAGE_WINDOW_MS;
|
|
3529
|
+
const sameSel = this.taps.filter((x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t);
|
|
3530
|
+
return { rage: sameSel.length >= RAGE_MIN_COUNT, count: sameSel.length };
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Evaluate a tap once its outcome window has closed. Classifies the post-tap
|
|
3534
|
+
* mutation diff into a semantic outcome and flags dead/rage.
|
|
3535
|
+
*/
|
|
3536
|
+
evaluate(tap) {
|
|
3537
|
+
const end = tap.t + OUTCOME_WINDOW_MS;
|
|
3538
|
+
const win = this.activity.filter((a) => a.t > tap.t && a.t <= end);
|
|
3539
|
+
const navigated = this.screenViews.some((t) => t > tap.t && t <= end);
|
|
3540
|
+
const sum = (f) => win.reduce((n, a) => n + f(a), 0);
|
|
3541
|
+
const structural = sum((a) => a.create) + sum((a) => a.remove) + sum((a) => a.move);
|
|
3542
|
+
const attrs = sum((a) => a.attr) + sum((a) => a.text);
|
|
3543
|
+
const mutationCount = structural + attrs;
|
|
3544
|
+
const overlayAdd = win.some((a) => a.overlayAdd);
|
|
3545
|
+
const overlayRemove = win.some((a) => a.overlayRemove);
|
|
3546
|
+
let kind;
|
|
3547
|
+
if (navigated) kind = "navigated";
|
|
3548
|
+
else if (overlayAdd) kind = "opened_overlay";
|
|
3549
|
+
else if (overlayRemove) kind = "closed_overlay";
|
|
3550
|
+
else if (mutationCount === 0) kind = "no_effect";
|
|
3551
|
+
else if (structural === 0 && attrs <= TOGGLE_ATTR_MAX) kind = "toggled";
|
|
3552
|
+
else kind = "content_changed";
|
|
3553
|
+
const from = tap.t - RAGE_WINDOW_MS;
|
|
3554
|
+
const rageCount = this.taps.filter(
|
|
3555
|
+
(x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t
|
|
3556
|
+
).length;
|
|
3557
|
+
let frustration = null;
|
|
3558
|
+
if (kind === "no_effect") frustration = { kind: "dead", count: 1 };
|
|
3559
|
+
if (rageCount >= RAGE_MIN_COUNT) frustration = { kind: "rage", count: rageCount };
|
|
3560
|
+
return { frustration, outcome: { kind, label: OUTCOME_LABELS[kind], mutationCount } };
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
var OVERLAY_TAGS = /* @__PURE__ */ new Set(["Modal"]);
|
|
3564
|
+
function isOverlayRecord(tag, attrs) {
|
|
3565
|
+
if (tag && OVERLAY_TAGS.has(tag)) return true;
|
|
3566
|
+
if (attrs && (attrs.wtOverlay === "true" || attrs.role === "dialog")) return true;
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
var Recorder = class {
|
|
3570
|
+
constructor() {
|
|
3571
|
+
__publicField(this, "mirror", new Mirror());
|
|
3572
|
+
__publicField(this, "index", emptyIndex());
|
|
3573
|
+
__publicField(this, "analyzer", new InteractionAnalyzer());
|
|
3574
|
+
__publicField(this, "stream", []);
|
|
3575
|
+
__publicField(this, "pending", []);
|
|
3576
|
+
__publicField(this, "viewport", { w: 0, h: 0 });
|
|
3577
|
+
}
|
|
3578
|
+
emit(t, m) {
|
|
3579
|
+
this.stream.push({ t, m });
|
|
3580
|
+
}
|
|
3581
|
+
setViewport(w, h, t) {
|
|
3582
|
+
this.viewport = { w, h };
|
|
3583
|
+
this.emit(t, [5, w, h]);
|
|
3584
|
+
}
|
|
3585
|
+
/** Feed a fresh tree snapshot; diffs against the last and streams mutations. */
|
|
3586
|
+
commit(tree, t) {
|
|
3587
|
+
const prev = this.index;
|
|
3588
|
+
const { messages, index } = diff(prev, tree, this.mirror);
|
|
3589
|
+
for (const m of messages) this.emit(t, m);
|
|
3590
|
+
const newIds = /* @__PURE__ */ new Set();
|
|
3591
|
+
for (const m of messages) {
|
|
3592
|
+
if (m[0] === 8 || m[0] === 9) newIds.add(m[1]);
|
|
3593
|
+
}
|
|
3594
|
+
let create = 0, remove = 0, move = 0, attr = 0, text = 0;
|
|
3595
|
+
let overlayAdd = false, overlayRemove = false;
|
|
3596
|
+
for (const m of messages) {
|
|
3597
|
+
switch (m[0]) {
|
|
3598
|
+
case 8:
|
|
3599
|
+
create++;
|
|
3600
|
+
if (isOverlayRecord(index.get(m[1])?.tag, index.get(m[1])?.attrs)) overlayAdd = true;
|
|
3601
|
+
break;
|
|
3602
|
+
case 9:
|
|
3603
|
+
create++;
|
|
3604
|
+
break;
|
|
3605
|
+
case 11:
|
|
3606
|
+
remove++;
|
|
3607
|
+
if (isOverlayRecord(prev.get(m[1])?.tag, prev.get(m[1])?.attrs)) overlayRemove = true;
|
|
3608
|
+
break;
|
|
3609
|
+
case 10:
|
|
3610
|
+
move++;
|
|
3611
|
+
break;
|
|
3612
|
+
case 12:
|
|
3613
|
+
case 13:
|
|
3614
|
+
if (!newIds.has(m[1])) attr++;
|
|
3615
|
+
break;
|
|
3616
|
+
case 14:
|
|
3617
|
+
if (!newIds.has(m[1])) text++;
|
|
3618
|
+
break;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const activity = { t, create, remove, move, attr, text, overlayAdd, overlayRemove };
|
|
3622
|
+
this.analyzer.recordActivity(activity);
|
|
3623
|
+
this.index = index;
|
|
3624
|
+
this.drainPending(t);
|
|
3625
|
+
}
|
|
3626
|
+
tap(input, t) {
|
|
3627
|
+
const [id] = this.mirror.getId(input.key);
|
|
3628
|
+
const selector = computeSelector(input.path);
|
|
3629
|
+
const label = computeLabel(input.attrs, input.text);
|
|
3630
|
+
const nx = Math.round(input.normX * 1e4);
|
|
3631
|
+
const ny = Math.round(input.normY * 1e4);
|
|
3632
|
+
this.emit(t, MobileTap(id, input.hesitationMs ?? 0, label, selector, nx, ny));
|
|
3633
|
+
this.analyzer.recordTap({ t, nodeId: id, selector });
|
|
3634
|
+
this.pending.push({ t, nodeId: id, selector });
|
|
3635
|
+
this.drainPending(t);
|
|
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));
|
|
3667
|
+
}
|
|
3668
|
+
/** Explicit clock advance (adapter ticker) to close outcome windows even when
|
|
3669
|
+
* no commit/tap arrives. */
|
|
3670
|
+
advance(t) {
|
|
3671
|
+
this.drainPending(t);
|
|
3672
|
+
}
|
|
3673
|
+
/** Flush all remaining pending taps (session end). `_t` is the session-end
|
|
3674
|
+
* clock, accepted for API symmetry with advance(); evaluation uses each tap's
|
|
3675
|
+
* own outcome window. */
|
|
3676
|
+
finalize(_t) {
|
|
3677
|
+
const ready = this.pending;
|
|
3678
|
+
this.pending = [];
|
|
3679
|
+
for (const p of ready) this.evaluateTap(p);
|
|
3680
|
+
this.stream.sort((a, b) => a.t - b.t);
|
|
3681
|
+
}
|
|
3682
|
+
getStream() {
|
|
3683
|
+
return [...this.stream].sort((a, b) => a.t - b.t);
|
|
3684
|
+
}
|
|
3685
|
+
/** Total messages emitted so far (insertion order). Adapters use this as the
|
|
3686
|
+
* cursor for incremental delta flushing — see `sliceStream`. */
|
|
3687
|
+
get emittedCount() {
|
|
3688
|
+
return this.stream.length;
|
|
3689
|
+
}
|
|
3690
|
+
/** Messages emitted since insertion index `from`, in EMISSION order (not
|
|
3691
|
+
* sorted by time). This is the delta an adapter ships each flush; the cursor
|
|
3692
|
+
* is the count it last confirmed stored. Late-drained outcome messages (whose
|
|
3693
|
+
* `t` is earlier than emission time) appear here at their emission position,
|
|
3694
|
+
* so the server appends verbatim and the reader sorts by `t`. */
|
|
3695
|
+
sliceStream(from) {
|
|
3696
|
+
return this.stream.slice(Math.max(0, from));
|
|
3697
|
+
}
|
|
3698
|
+
get viewportSize() {
|
|
3699
|
+
return this.viewport;
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
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;
|
|
3712
|
+
}
|
|
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
|
+
};
|
|
3751
|
+
}
|
|
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;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
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);
|
|
3849
|
+
}
|
|
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;
|
|
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;
|
|
3879
|
+
}
|
|
3880
|
+
return out;
|
|
3881
|
+
}
|
|
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;
|
|
3887
|
+
}
|
|
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;
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
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 };
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
// src/react-native/structural/capture.ts
|
|
3925
|
+
var WALK_BUDGET_MS = 24;
|
|
3926
|
+
var MAX_OVERLOAD_SKIP = 8;
|
|
3927
|
+
function genSessionId() {
|
|
3928
|
+
const rand = Math.floor(Math.random() * 16777215).toString(36);
|
|
3929
|
+
return `wt-${Date.now().toString(36)}-${rand}`;
|
|
3930
|
+
}
|
|
3931
|
+
var StructuralCapture = class {
|
|
3932
|
+
constructor() {
|
|
3933
|
+
this.recorder = new Recorder();
|
|
3934
|
+
this.currentIndex = /* @__PURE__ */ new Map();
|
|
3935
|
+
this.started = 0;
|
|
3936
|
+
this.sampleTimer = null;
|
|
3937
|
+
this.flushTimer = null;
|
|
3938
|
+
this.screen = "";
|
|
3939
|
+
this.prevScreen = "";
|
|
3940
|
+
this.cfg = null;
|
|
3941
|
+
this.sessionId = "";
|
|
3942
|
+
this.running = false;
|
|
3943
|
+
// Incremental delivery: cursor into the recorder's emission stream that has
|
|
3944
|
+
// been CONFIRMED stored. Everything at index >= sentCount is unsent (the
|
|
3945
|
+
// offline buffer). Advances only on a 2xx.
|
|
3946
|
+
this.sentCount = 0;
|
|
3947
|
+
this.flushing = false;
|
|
3948
|
+
// Persistent per-ScrollView content offset (keyed by native tag), so scroll
|
|
3949
|
+
// continues from where it was rather than resetting each gesture.
|
|
3950
|
+
this.scrollOffsets = /* @__PURE__ */ new Map();
|
|
3951
|
+
// Adaptive sampling: skip this many upcoming ticks after a heavy walk.
|
|
3952
|
+
this.overloadSkip = 0;
|
|
3953
|
+
this.appStateSub = null;
|
|
3954
|
+
this.teardowns = [];
|
|
3955
|
+
}
|
|
3956
|
+
now() {
|
|
3957
|
+
return Date.now() - this.started;
|
|
3958
|
+
}
|
|
3959
|
+
isRunning() {
|
|
3960
|
+
return this.running;
|
|
3961
|
+
}
|
|
3962
|
+
getSessionId() {
|
|
3963
|
+
return this.sessionId;
|
|
3964
|
+
}
|
|
3965
|
+
start(cfg) {
|
|
3966
|
+
if (this.running) return;
|
|
3967
|
+
installStructuralHook();
|
|
3968
|
+
this.cfg = cfg;
|
|
3969
|
+
this.sessionId = cfg.sessionId || genSessionId();
|
|
3970
|
+
this.started = Date.now();
|
|
3971
|
+
this.running = true;
|
|
3972
|
+
this.sentCount = 0;
|
|
3973
|
+
this.scrollOffsets.clear();
|
|
3974
|
+
this.overloadSkip = 0;
|
|
3975
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
3976
|
+
this.recorder.setViewport(Math.round(width), Math.round(height), 0);
|
|
3977
|
+
this.sampleTimer = setInterval(() => this.sample(), cfg.sampleIntervalMs ?? 120);
|
|
3978
|
+
this.flushTimer = setInterval(() => void this.flush(), cfg.flushIntervalMs ?? 4e3);
|
|
3979
|
+
this.installCrashFlush();
|
|
3980
|
+
this.installBackgroundFlush();
|
|
3981
|
+
if (cfg.debug) console.log("[structural] started", this.sessionId);
|
|
3982
|
+
setTimeout(() => this.sample(true), 0);
|
|
3983
|
+
}
|
|
3984
|
+
/** Best-effort flush the pre-crash tail before the app tears down. */
|
|
3985
|
+
installCrashFlush() {
|
|
3986
|
+
if (typeof ErrorUtils === "undefined" || typeof ErrorUtils.setGlobalHandler !== "function") return;
|
|
3987
|
+
const prev = ErrorUtils.getGlobalHandler?.();
|
|
3988
|
+
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
3989
|
+
void this.flush();
|
|
3990
|
+
if (typeof prev === "function") prev(error, isFatal);
|
|
3991
|
+
});
|
|
3992
|
+
this.teardowns.push(() => {
|
|
3993
|
+
if (typeof prev === "function") ErrorUtils.setGlobalHandler(prev);
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
/** Flush on background/inactive so timer suspension doesn't strand the tail. */
|
|
3997
|
+
installBackgroundFlush() {
|
|
3998
|
+
const onChange = (state) => {
|
|
3999
|
+
if (state === "background" || state === "inactive") void this.flush();
|
|
4000
|
+
};
|
|
4001
|
+
this.appStateSub = reactNative.AppState.addEventListener("change", onChange);
|
|
4002
|
+
this.teardowns.push(() => {
|
|
4003
|
+
this.appStateSub?.remove();
|
|
4004
|
+
this.appStateSub = null;
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
sample(force = false) {
|
|
4008
|
+
if (!this.running) return;
|
|
4009
|
+
if (!force && this.overloadSkip > 0) {
|
|
4010
|
+
this.overloadSkip--;
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
if (!force && !hookState.dirty) return;
|
|
4014
|
+
hookState.dirty = false;
|
|
4015
|
+
const root = hookState.latestRoot;
|
|
4016
|
+
if (!root) return;
|
|
4017
|
+
const t0 = Date.now();
|
|
4018
|
+
try {
|
|
4019
|
+
const { tree, index } = walkFiberRoot(root);
|
|
4020
|
+
this.currentIndex = index;
|
|
4021
|
+
this.recorder.commit(tree, this.now());
|
|
4022
|
+
} catch (e) {
|
|
4023
|
+
if (this.cfg?.debug) console.log("[structural] sample error", String(e));
|
|
4024
|
+
}
|
|
4025
|
+
const cost = Date.now() - t0;
|
|
4026
|
+
if (cost > WALK_BUDGET_MS) {
|
|
4027
|
+
this.overloadSkip = Math.min(MAX_OVERLOAD_SKIP, Math.ceil(cost / WALK_BUDGET_MS));
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
setScreen(name) {
|
|
4031
|
+
if (!this.running || name === this.screen) return;
|
|
4032
|
+
this.prevScreen = this.screen;
|
|
4033
|
+
this.screen = name;
|
|
4034
|
+
this.sample(true);
|
|
4035
|
+
this.recorder.screenView(name, this.prevScreen, this.now());
|
|
4036
|
+
}
|
|
4037
|
+
/** Resolve a native touch to a node + emit a tap. `target` is the RN
|
|
4038
|
+
* nativeTag from `nativeEvent.target`. */
|
|
4039
|
+
handleTap(target, pageX, pageY) {
|
|
4040
|
+
if (!this.running) return;
|
|
4041
|
+
const entry = this.currentIndex.get(target);
|
|
4042
|
+
if (!entry) return;
|
|
4043
|
+
const { width, height } = reactNative.Dimensions.get("window");
|
|
4044
|
+
this.recorder.tap(
|
|
4045
|
+
{
|
|
4046
|
+
key: entry.node.key,
|
|
4047
|
+
path: entry.path,
|
|
4048
|
+
attrs: entry.node.attrs,
|
|
4049
|
+
text: entry.node.text,
|
|
4050
|
+
normX: pageX / (width || 1),
|
|
4051
|
+
normY: pageY / (height || 1)
|
|
4052
|
+
},
|
|
4053
|
+
this.now()
|
|
4054
|
+
);
|
|
4055
|
+
}
|
|
4056
|
+
/** Feed a scroll/drag delta from a touch onto the nearest scrollable ancestor
|
|
4057
|
+
* of the touched node. `target` is the RN nativeTag under the finger; `dx`/`dy`
|
|
4058
|
+
* are content-offset deltas (px, sign = content motion). Native-driven scroll
|
|
4059
|
+
* never reaches the fiber walk, so this is the only way the replay can ride the
|
|
4060
|
+
* scroll instead of freezing on a stale frame. */
|
|
4061
|
+
handleScroll(target, dx, dy) {
|
|
4062
|
+
if (!this.running) return;
|
|
4063
|
+
const leaf = this.currentIndex.get(target);
|
|
4064
|
+
const scrollTag = leaf?.scrollAncestorTag;
|
|
4065
|
+
if (scrollTag === void 0) return;
|
|
4066
|
+
const scrollable = this.currentIndex.get(scrollTag);
|
|
4067
|
+
if (!scrollable) return;
|
|
4068
|
+
const cur = this.scrollOffsets.get(scrollTag) ?? { x: 0, y: 0 };
|
|
4069
|
+
const next = { x: Math.max(0, cur.x + dx), y: Math.max(0, cur.y + dy) };
|
|
4070
|
+
if (Math.abs(next.x - cur.x) < 1 && Math.abs(next.y - cur.y) < 1) return;
|
|
4071
|
+
this.scrollOffsets.set(scrollTag, next);
|
|
4072
|
+
this.recorder.scroll(scrollable.node.key, next.x, next.y, this.now());
|
|
4073
|
+
}
|
|
4074
|
+
/** Public best-effort flush — used by the shared RN error handler to push the
|
|
4075
|
+
* pre-crash tail (the in-class ErrorUtils hook covers apps that don't call
|
|
4076
|
+
* setupRNErrorHandler; both are idempotent via the `flushing` guard). */
|
|
4077
|
+
async flushNow() {
|
|
4078
|
+
await this.flush();
|
|
4079
|
+
}
|
|
4080
|
+
async flush() {
|
|
4081
|
+
if (!this.cfg || this.flushing) return;
|
|
4082
|
+
this.recorder.advance(this.now());
|
|
4083
|
+
const from = this.sentCount;
|
|
4084
|
+
const batch = this.recorder.sliceStream(from);
|
|
4085
|
+
if (batch.length === 0) return;
|
|
4086
|
+
this.flushing = true;
|
|
4087
|
+
const vp = this.recorder.viewportSize;
|
|
4088
|
+
try {
|
|
4089
|
+
const headers = {
|
|
4090
|
+
"content-type": "application/json",
|
|
4091
|
+
"X-API-Key": this.cfg.apiKey
|
|
4092
|
+
};
|
|
4093
|
+
if (this.cfg.projectId) headers["X-Project-ID"] = this.cfg.projectId;
|
|
4094
|
+
const res = await fetch(`${this.cfg.endpoint.replace(/\/$/, "")}/api/replay-stream`, {
|
|
4095
|
+
method: "POST",
|
|
4096
|
+
headers,
|
|
4097
|
+
body: JSON.stringify({
|
|
4098
|
+
session_id: this.sessionId,
|
|
4099
|
+
platform: this.cfg.platform,
|
|
4100
|
+
viewport: { w: vp.w, h: vp.h },
|
|
4101
|
+
seq_start: from,
|
|
4102
|
+
messages: batch
|
|
4103
|
+
})
|
|
4104
|
+
});
|
|
4105
|
+
if (res.ok) {
|
|
4106
|
+
this.sentCount = from + batch.length;
|
|
4107
|
+
if (this.cfg.debug) console.log("[structural] flushed", batch.length, "msgs @", from);
|
|
4108
|
+
} else if (this.cfg.debug) {
|
|
4109
|
+
console.log("[structural] flush rejected", res.status, "\u2014 will retry");
|
|
4110
|
+
}
|
|
4111
|
+
} catch (e) {
|
|
4112
|
+
if (this.cfg.debug) console.log("[structural] flush failed", String(e), "\u2014 buffered");
|
|
4113
|
+
} finally {
|
|
4114
|
+
this.flushing = false;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
async stop() {
|
|
4118
|
+
if (!this.running) return;
|
|
4119
|
+
this.recorder.finalize(this.now());
|
|
4120
|
+
if (this.sampleTimer) clearInterval(this.sampleTimer);
|
|
4121
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
4122
|
+
this.sampleTimer = null;
|
|
4123
|
+
this.flushTimer = null;
|
|
4124
|
+
this.running = false;
|
|
4125
|
+
for (const t of this.teardowns.splice(0)) {
|
|
4126
|
+
try {
|
|
4127
|
+
t();
|
|
4128
|
+
} catch {
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
await this.flush();
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
var structuralCapture = new StructuralCapture();
|
|
4135
|
+
|
|
3315
4136
|
// src/react-native/error-handler.ts
|
|
3316
4137
|
function setupRNErrorHandler(options = {}) {
|
|
3317
4138
|
const {
|
|
@@ -3332,6 +4153,10 @@ function setupRNErrorHandler(options = {}) {
|
|
|
3332
4153
|
});
|
|
3333
4154
|
flushScreenRecording().catch(() => {
|
|
3334
4155
|
});
|
|
4156
|
+
if (structuralCapture.isRunning()) {
|
|
4157
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
3335
4160
|
}
|
|
3336
4161
|
if (previousHandler) {
|
|
3337
4162
|
previousHandler(error, isFatal);
|
|
@@ -3352,6 +4177,10 @@ function setupRNErrorHandler(options = {}) {
|
|
|
3352
4177
|
});
|
|
3353
4178
|
flushScreenRecording().catch(() => {
|
|
3354
4179
|
});
|
|
4180
|
+
if (structuralCapture.isRunning()) {
|
|
4181
|
+
void structuralCapture.flushNow().catch(() => {
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
3355
4184
|
};
|
|
3356
4185
|
if (typeof globalThis.addEventListener === "function") {
|
|
3357
4186
|
globalThis.addEventListener("unhandledrejection", rejectionHandler);
|
|
@@ -3905,6 +4734,60 @@ function WatchtowerProvider(props) {
|
|
|
3905
4734
|
}, []);
|
|
3906
4735
|
return React2__default.default.createElement(React2__default.default.Fragment, null, children);
|
|
3907
4736
|
}
|
|
4737
|
+
var TAP_MOVE_THRESHOLD_PX = 12;
|
|
4738
|
+
function StructuralCaptureProvider({ children, ...cfg }) {
|
|
4739
|
+
const cfgRef = React2.useRef(cfg);
|
|
4740
|
+
cfgRef.current = cfg;
|
|
4741
|
+
React2.useEffect(() => {
|
|
4742
|
+
structuralCapture.start(cfgRef.current);
|
|
4743
|
+
return () => {
|
|
4744
|
+
void structuralCapture.stop();
|
|
4745
|
+
};
|
|
4746
|
+
}, []);
|
|
4747
|
+
const gesture = React2.useRef(null);
|
|
4748
|
+
const onTouchStart = (e) => {
|
|
4749
|
+
const ne = e.nativeEvent;
|
|
4750
|
+
gesture.current = {
|
|
4751
|
+
startX: ne.pageX,
|
|
4752
|
+
startY: ne.pageY,
|
|
4753
|
+
lastX: ne.pageX,
|
|
4754
|
+
lastY: ne.pageY,
|
|
4755
|
+
target: Number(ne.target),
|
|
4756
|
+
scrolling: false
|
|
4757
|
+
};
|
|
4758
|
+
};
|
|
4759
|
+
const onTouchMove = (e) => {
|
|
4760
|
+
const g2 = gesture.current;
|
|
4761
|
+
if (!g2) return;
|
|
4762
|
+
const ne = e.nativeEvent;
|
|
4763
|
+
const movedFar = Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX;
|
|
4764
|
+
if (movedFar) g2.scrolling = true;
|
|
4765
|
+
if (g2.scrolling) {
|
|
4766
|
+
const dx = g2.lastX - ne.pageX;
|
|
4767
|
+
const dy = g2.lastY - ne.pageY;
|
|
4768
|
+
structuralCapture.handleScroll(g2.target, dx, dy);
|
|
4769
|
+
}
|
|
4770
|
+
g2.lastX = ne.pageX;
|
|
4771
|
+
g2.lastY = ne.pageY;
|
|
4772
|
+
};
|
|
4773
|
+
const onTouchEnd = (e) => {
|
|
4774
|
+
const g2 = gesture.current;
|
|
4775
|
+
gesture.current = null;
|
|
4776
|
+
if (!g2) return;
|
|
4777
|
+
if (g2.scrolling) return;
|
|
4778
|
+
const ne = e.nativeEvent;
|
|
4779
|
+
if (Math.abs(ne.pageX - g2.startX) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - g2.startY) > TAP_MOVE_THRESHOLD_PX) {
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
structuralCapture.handleTap(g2.target, ne.pageX, ne.pageY);
|
|
4783
|
+
};
|
|
4784
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, onTouchStart, onTouchMove, onTouchEnd, children });
|
|
4785
|
+
}
|
|
4786
|
+
function useStructuralScreen(name) {
|
|
4787
|
+
React2.useEffect(() => {
|
|
4788
|
+
structuralCapture.setScreen(name);
|
|
4789
|
+
}, [name]);
|
|
4790
|
+
}
|
|
3908
4791
|
|
|
3909
4792
|
// src/react-native/sqlite-tracker.ts
|
|
3910
4793
|
function now() {
|
|
@@ -4326,6 +5209,8 @@ exports.RNAutocaptureModule = RNAutocaptureModule;
|
|
|
4326
5209
|
exports.RNPerformanceManager = RNPerformanceManager;
|
|
4327
5210
|
exports.SensitiveView = SensitiveView;
|
|
4328
5211
|
exports.SitePongRNProvider = SitePongRNProvider;
|
|
5212
|
+
exports.StructuralCapture = StructuralCapture;
|
|
5213
|
+
exports.StructuralCaptureProvider = StructuralCaptureProvider;
|
|
4329
5214
|
exports.WatchtowerProvider = WatchtowerProvider;
|
|
4330
5215
|
exports.addBreadcrumb = addBreadcrumb;
|
|
4331
5216
|
exports.addSensitiveView = addSensitiveView;
|
|
@@ -4360,6 +5245,7 @@ exports.handleUniversalLink = handleUniversalLink;
|
|
|
4360
5245
|
exports.identify = identify2;
|
|
4361
5246
|
exports.initRN = initRN;
|
|
4362
5247
|
exports.initSuperLinkRN = initSuperLinkRN;
|
|
5248
|
+
exports.installStructuralHook = installStructuralHook;
|
|
4363
5249
|
exports.isInitialized = isInitialized;
|
|
4364
5250
|
exports.isRemoteConfigFeatureEnabled = isRemoteConfigFeatureEnabled;
|
|
4365
5251
|
exports.isScreenRecording = isScreenRecording;
|
|
@@ -4391,6 +5277,7 @@ exports.startWatchtower = startWatchtower;
|
|
|
4391
5277
|
exports.stopScreenRecording = stopScreenRecording;
|
|
4392
5278
|
exports.stopWatchtower = stopWatchtower;
|
|
4393
5279
|
exports.stopWidgetSync = stopWidgetSync;
|
|
5280
|
+
exports.structuralCapture = structuralCapture;
|
|
4394
5281
|
exports.subscribeNavigationToWatchtower = subscribeNavigationToWatchtower;
|
|
4395
5282
|
exports.superlinkIdentify = identify3;
|
|
4396
5283
|
exports.syncWidgets = syncWidgets;
|
|
@@ -4403,6 +5290,7 @@ exports.useRNPerformance = useRNPerformance;
|
|
|
4403
5290
|
exports.useRemoteConfig = useRemoteConfig;
|
|
4404
5291
|
exports.useScreenTrack = useScreenTrack;
|
|
4405
5292
|
exports.useSitePongRNContext = useSitePongRNContext;
|
|
5293
|
+
exports.useStructuralScreen = useStructuralScreen;
|
|
4406
5294
|
exports.waitForFlags = waitForFlags;
|
|
4407
5295
|
//# sourceMappingURL=rn.js.map
|
|
4408
5296
|
//# sourceMappingURL=rn.js.map
|