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.
@@ -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 g = globalThis;
46
- const ErrorUtils2 = g.ErrorUtils;
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 g = globalThis;
99
- if (g.navigator && g.navigator.product === "ReactNative") return "react-native";
100
- if (g.process && g.process.versions && g.process.versions.node) return "node";
101
- if (g.window && g.document) return "browser";
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 g = globalThis;
2142
- const hasBrowser = typeof g.window !== "undefined" && typeof g.document !== "undefined";
2143
- const hasNode = !hasBrowser && !!g.process?.versions?.node;
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 (g.navigator?.userAgent) info.userAgent = g.navigator.userAgent;
2151
- if (hasBrowser && g.window?.screen) {
2152
- info.screenWidth = g.window.screen.width;
2153
- info.screenHeight = g.window.screen.height;
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 && g.process) {
2156
- info.osName = g.process.platform;
2157
- info.osVersion = g.process.versions?.node;
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 g.window?.location?.href ?? void 0;
2165
+ return g2.window?.location?.href ?? void 0;
2163
2166
  },
2164
2167
  getReferrer() {
2165
- return g.document?.referrer || void 0;
2168
+ return g2.document?.referrer || void 0;
2166
2169
  },
2167
2170
  getUserAgent() {
2168
- if (g.navigator?.userAgent) return g.navigator.userAgent;
2169
- if (hasNode && g.process) return `SitePong-Node/${g.process.versions?.node} (${g.process.platform})`;
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 && g.window?.addEventListener) {
2176
+ if (hasBrowser && g2.window?.addEventListener) {
2174
2177
  const listener = (event) => handler(event.error || event.message);
2175
- g.window.addEventListener("error", listener);
2176
- return () => g.window?.removeEventListener?.("error", listener);
2178
+ g2.window.addEventListener("error", listener);
2179
+ return () => g2.window?.removeEventListener?.("error", listener);
2177
2180
  }
2178
- if (hasNode && g.process?.on && g.process.off) {
2181
+ if (hasNode && g2.process?.on && g2.process.off) {
2179
2182
  const listener = (err) => handler(err);
2180
- g.process.on("uncaughtException", listener);
2181
- return () => g.process?.off?.("uncaughtException", listener);
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 && g.window?.addEventListener) {
2190
+ if (hasBrowser && g2.window?.addEventListener) {
2188
2191
  const listener = (event) => handler(event.reason);
2189
- g.window.addEventListener("unhandledrejection", listener);
2190
- return () => g.window?.removeEventListener?.("unhandledrejection", listener);
2192
+ g2.window.addEventListener("unhandledrejection", listener);
2193
+ return () => g2.window?.removeEventListener?.("unhandledrejection", listener);
2191
2194
  }
2192
- if (hasNode && g.process?.on && g.process.off) {
2195
+ if (hasNode && g2.process?.on && g2.process.off) {
2193
2196
  const listener = (reason) => handler(reason);
2194
- g.process.on("unhandledRejection", listener);
2195
- return () => g.process?.off?.("unhandledRejection", listener);
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 && g.window?.addEventListener) {
2202
- g.window.addEventListener("beforeunload", handler);
2203
- return () => g.window?.removeEventListener?.("beforeunload", handler);
2204
+ if (hasBrowser && g2.window?.addEventListener) {
2205
+ g2.window.addEventListener("beforeunload", handler);
2206
+ return () => g2.window?.removeEventListener?.("beforeunload", handler);
2204
2207
  }
2205
- if (hasNode && g.process?.on && g.process.off) {
2206
- g.process.on("beforeExit", handler);
2207
- g.process.on("SIGINT", handler);
2208
- g.process.on("SIGTERM", handler);
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
- g.process?.off?.("beforeExit", handler);
2211
- g.process?.off?.("SIGINT", handler);
2212
- g.process?.off?.("SIGTERM", handler);
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 (g.document?.addEventListener) {
2222
+ if (g2.document?.addEventListener) {
2220
2223
  const listener = () => {
2221
- if (g.document?.visibilityState === "hidden") handler();
2224
+ if (g2.document?.visibilityState === "hidden") handler();
2222
2225
  };
2223
- g.document.addEventListener("visibilitychange", listener);
2224
- return () => g.document?.removeEventListener?.("visibilitychange", listener);
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 g.navigator?.sendBeacon !== "function") return false;
2233
+ if (typeof g2.navigator?.sendBeacon !== "function") return false;
2231
2234
  try {
2232
- return g.navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
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