sitepong 0.2.6 → 0.2.8

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
  }
@@ -3906,6 +3909,732 @@ function WatchtowerProvider(props) {
3906
3909
  return React2__default.default.createElement(React2__default.default.Fragment, null, children);
3907
3910
  }
3908
3911
 
3912
+ // ../watchtower-recorder/dist/index.mjs
3913
+ var CreateElementNode = (id, parentID, index, tag) => [8, id, parentID, index, tag];
3914
+ var CreateTextNode = (id, parentID, index) => [
3915
+ 9,
3916
+ id,
3917
+ parentID,
3918
+ index
3919
+ ];
3920
+ var MoveNode = (id, parentID, index) => [
3921
+ 10,
3922
+ id,
3923
+ parentID,
3924
+ index
3925
+ ];
3926
+ var RemoveNode = (id) => [11, id];
3927
+ var SetNodeAttribute = (id, name, value) => [
3928
+ 12,
3929
+ id,
3930
+ name,
3931
+ value
3932
+ ];
3933
+ var RemoveNodeAttribute = (id, name) => [
3934
+ 13,
3935
+ id,
3936
+ name
3937
+ ];
3938
+ var SetNodeData = (id, data) => [14, id, data];
3939
+ var MobileTap = (id, hesitationMs, label, selector, normX, normY) => [68, id, hesitationMs, label, selector, normX, normY];
3940
+ var ScreenView = (name, prevName) => [
3941
+ 200,
3942
+ name,
3943
+ prevName
3944
+ ];
3945
+ var Frustration = (kind, nodeId, selector, count) => [201, kind, nodeId, selector, count];
3946
+ var TapOutcome = (tapNodeId, kind, label, mutationCount) => [202, tapNodeId, kind, label, mutationCount];
3947
+ function walk(root, visit) {
3948
+ const rec = (node, parent, index) => {
3949
+ visit(node, parent, index);
3950
+ node.children.forEach((child, i) => rec(child, node, i));
3951
+ };
3952
+ if (root) rec(root, null, 0);
3953
+ }
3954
+ var Mirror = class {
3955
+ constructor() {
3956
+ __publicField(this, "ids", /* @__PURE__ */ new WeakMap());
3957
+ __publicField(this, "byId", /* @__PURE__ */ new Map());
3958
+ __publicField(this, "next", 1);
3959
+ }
3960
+ // 0 is reserved for "no/root parent"
3961
+ /** Existing id for a node, or undefined if never registered. */
3962
+ peek(key) {
3963
+ return this.ids.get(key);
3964
+ }
3965
+ /** Id for a node, assigning a fresh one on first sight. Second tuple element
3966
+ * is true when the id was just created (→ emit a Create* message). */
3967
+ getId(key) {
3968
+ const existing = this.ids.get(key);
3969
+ if (existing !== void 0) return [existing, false];
3970
+ const id = this.next++;
3971
+ this.ids.set(key, id);
3972
+ this.byId.set(id, key);
3973
+ return [id, true];
3974
+ }
3975
+ getKey(id) {
3976
+ return this.byId.get(id);
3977
+ }
3978
+ /** Drop a node's id (on unmount). Idempotent. */
3979
+ release(key) {
3980
+ const id = this.ids.get(key);
3981
+ if (id === void 0) return void 0;
3982
+ this.ids.delete(key);
3983
+ this.byId.delete(id);
3984
+ return id;
3985
+ }
3986
+ get size() {
3987
+ return this.byId.size;
3988
+ }
3989
+ };
3990
+ var emptyIndex = () => /* @__PURE__ */ new Map();
3991
+ function indexTree(tree, mirror, prev) {
3992
+ const next = /* @__PURE__ */ new Map();
3993
+ const order = [];
3994
+ walk(tree, (node, parent, idx) => {
3995
+ const [id] = mirror.getId(node.key);
3996
+ const parentId = parent ? mirror.getId(parent.key)[0] : 0;
3997
+ next.set(id, {
3998
+ id,
3999
+ parentId,
4000
+ index: idx,
4001
+ tag: node.tag,
4002
+ attrs: node.attrs,
4003
+ text: node.text
4004
+ });
4005
+ order.push(id);
4006
+ });
4007
+ return { next, order };
4008
+ }
4009
+ function attrDiff(id, prev, next) {
4010
+ const out = [];
4011
+ for (const k of Object.keys(next)) {
4012
+ if (prev[k] !== next[k]) out.push(SetNodeAttribute(id, k, next[k]));
4013
+ }
4014
+ for (const k of Object.keys(prev)) {
4015
+ if (!(k in next)) out.push(RemoveNodeAttribute(id, k));
4016
+ }
4017
+ return out;
4018
+ }
4019
+ function diff(prev, next, mirror) {
4020
+ const { next: nextIndex, order } = indexTree(next, mirror);
4021
+ const messages = [];
4022
+ const removed = /* @__PURE__ */ new Set();
4023
+ for (const id of prev.keys()) if (!nextIndex.has(id)) removed.add(id);
4024
+ for (const id of removed) {
4025
+ const rec = prev.get(id);
4026
+ if (!removed.has(rec.parentId)) messages.push(RemoveNode(id));
4027
+ }
4028
+ for (const id of order) {
4029
+ const rec = nextIndex.get(id);
4030
+ const before = prev.get(id);
4031
+ if (!before) {
4032
+ if (rec.tag === "#text") {
4033
+ messages.push(CreateTextNode(id, rec.parentId, rec.index));
4034
+ } else {
4035
+ messages.push(CreateElementNode(id, rec.parentId, rec.index, rec.tag));
4036
+ }
4037
+ for (const k of Object.keys(rec.attrs)) messages.push(SetNodeAttribute(id, k, rec.attrs[k]));
4038
+ if (rec.text !== void 0 && rec.text !== "") messages.push(SetNodeData(id, rec.text));
4039
+ } else {
4040
+ if (before.parentId !== rec.parentId || before.index !== rec.index) {
4041
+ messages.push(MoveNode(id, rec.parentId, rec.index));
4042
+ }
4043
+ messages.push(...attrDiff(id, before.attrs, rec.attrs));
4044
+ if ((before.text ?? "") !== (rec.text ?? "")) messages.push(SetNodeData(id, rec.text ?? ""));
4045
+ }
4046
+ }
4047
+ return { messages, index: nextIndex };
4048
+ }
4049
+ function stableAnchor(step) {
4050
+ if (step.attrs.testID) return `@${step.attrs.testID}`;
4051
+ if (step.attrs.nativeID) return `#${step.attrs.nativeID}`;
4052
+ return null;
4053
+ }
4054
+ function computeSelector(path) {
4055
+ if (path.length === 0) return "";
4056
+ const self = path[path.length - 1];
4057
+ const own = stableAnchor(self);
4058
+ if (own) return own;
4059
+ const parts = [];
4060
+ for (let i = path.length - 1; i >= 0; i--) {
4061
+ const step = path[i];
4062
+ const anchor = stableAnchor(step);
4063
+ if (anchor && i !== path.length - 1) {
4064
+ parts.unshift(anchor);
4065
+ return parts.join(" > ");
4066
+ }
4067
+ const a11y = step.attrs.accessibilityLabel;
4068
+ const comp = step.attrs.rnComponent;
4069
+ if (i === path.length - 1 && a11y) {
4070
+ parts.unshift(`${step.tag}[a11y="${truncate(a11y)}"]`);
4071
+ } else if (comp && comp !== step.tag) {
4072
+ parts.unshift(`${comp}`);
4073
+ } else {
4074
+ parts.unshift(`${step.tag}:nth-of-type(${step.nthOfType})`);
4075
+ }
4076
+ }
4077
+ return parts.join(" > ");
4078
+ }
4079
+ function truncate(s, n = 40) {
4080
+ return s.length > n ? s.slice(0, n) : s;
4081
+ }
4082
+ function computeLabel(attrs, text) {
4083
+ if (attrs.accessibilityLabel) return truncate(attrs.accessibilityLabel, 100);
4084
+ if (attrs.testID) return attrs.testID;
4085
+ if (text && text.trim()) return truncate(text.trim(), 100);
4086
+ if (attrs.rnComponent) return attrs.rnComponent;
4087
+ return "";
4088
+ }
4089
+ var RAGE_WINDOW_MS = 800;
4090
+ var RAGE_MIN_COUNT = 3;
4091
+ var OUTCOME_WINDOW_MS = 1e3;
4092
+ var TOGGLE_ATTR_MAX = 4;
4093
+ var OUTCOME_LABELS = {
4094
+ navigated: "Navigated to a new screen",
4095
+ opened_overlay: "Opened a modal / overlay",
4096
+ closed_overlay: "Dismissed a modal / overlay",
4097
+ toggled: "Toggled UI state",
4098
+ content_changed: "Updated on-screen content",
4099
+ no_effect: "Nothing changed (dead tap)"
4100
+ };
4101
+ var InteractionAnalyzer = class {
4102
+ constructor() {
4103
+ __publicField(this, "activity", []);
4104
+ __publicField(this, "screenViews", []);
4105
+ __publicField(this, "taps", []);
4106
+ }
4107
+ recordActivity(a) {
4108
+ this.activity.push(a);
4109
+ }
4110
+ recordScreenView(t) {
4111
+ this.screenViews.push(t);
4112
+ }
4113
+ /** Register a tap; returns whether it is (so far) a rage tap and the count. */
4114
+ recordTap(tap) {
4115
+ this.taps.push(tap);
4116
+ const from = tap.t - RAGE_WINDOW_MS;
4117
+ const sameSel = this.taps.filter((x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t);
4118
+ return { rage: sameSel.length >= RAGE_MIN_COUNT, count: sameSel.length };
4119
+ }
4120
+ /**
4121
+ * Evaluate a tap once its outcome window has closed. Classifies the post-tap
4122
+ * mutation diff into a semantic outcome and flags dead/rage.
4123
+ */
4124
+ evaluate(tap) {
4125
+ const end = tap.t + OUTCOME_WINDOW_MS;
4126
+ const win = this.activity.filter((a) => a.t > tap.t && a.t <= end);
4127
+ const navigated = this.screenViews.some((t) => t > tap.t && t <= end);
4128
+ const sum = (f) => win.reduce((n, a) => n + f(a), 0);
4129
+ const structural = sum((a) => a.create) + sum((a) => a.remove) + sum((a) => a.move);
4130
+ const attrs = sum((a) => a.attr) + sum((a) => a.text);
4131
+ const mutationCount = structural + attrs;
4132
+ const overlayAdd = win.some((a) => a.overlayAdd);
4133
+ const overlayRemove = win.some((a) => a.overlayRemove);
4134
+ let kind;
4135
+ if (navigated) kind = "navigated";
4136
+ else if (overlayAdd) kind = "opened_overlay";
4137
+ else if (overlayRemove) kind = "closed_overlay";
4138
+ else if (mutationCount === 0) kind = "no_effect";
4139
+ else if (structural === 0 && attrs <= TOGGLE_ATTR_MAX) kind = "toggled";
4140
+ else kind = "content_changed";
4141
+ const from = tap.t - RAGE_WINDOW_MS;
4142
+ const rageCount = this.taps.filter(
4143
+ (x) => x.selector === tap.selector && x.t >= from && x.t <= tap.t
4144
+ ).length;
4145
+ let frustration = null;
4146
+ if (kind === "no_effect") frustration = { kind: "dead", count: 1 };
4147
+ if (rageCount >= RAGE_MIN_COUNT) frustration = { kind: "rage", count: rageCount };
4148
+ return { frustration, outcome: { kind, label: OUTCOME_LABELS[kind], mutationCount } };
4149
+ }
4150
+ };
4151
+ var OVERLAY_TAGS = /* @__PURE__ */ new Set(["Modal"]);
4152
+ function isOverlayRecord(tag, attrs) {
4153
+ if (tag && OVERLAY_TAGS.has(tag)) return true;
4154
+ if (attrs && (attrs.wtOverlay === "true" || attrs.role === "dialog")) return true;
4155
+ return false;
4156
+ }
4157
+ var Recorder = class {
4158
+ constructor() {
4159
+ __publicField(this, "mirror", new Mirror());
4160
+ __publicField(this, "index", emptyIndex());
4161
+ __publicField(this, "analyzer", new InteractionAnalyzer());
4162
+ __publicField(this, "stream", []);
4163
+ __publicField(this, "pending", []);
4164
+ __publicField(this, "viewport", { w: 0, h: 0 });
4165
+ }
4166
+ emit(t, m) {
4167
+ this.stream.push({ t, m });
4168
+ }
4169
+ setViewport(w, h, t) {
4170
+ this.viewport = { w, h };
4171
+ this.emit(t, [5, w, h]);
4172
+ }
4173
+ /** Feed a fresh tree snapshot; diffs against the last and streams mutations. */
4174
+ commit(tree, t) {
4175
+ const prev = this.index;
4176
+ const { messages, index } = diff(prev, tree, this.mirror);
4177
+ for (const m of messages) this.emit(t, m);
4178
+ const newIds = /* @__PURE__ */ new Set();
4179
+ for (const m of messages) {
4180
+ if (m[0] === 8 || m[0] === 9) newIds.add(m[1]);
4181
+ }
4182
+ let create = 0, remove = 0, move = 0, attr = 0, text = 0;
4183
+ let overlayAdd = false, overlayRemove = false;
4184
+ for (const m of messages) {
4185
+ switch (m[0]) {
4186
+ case 8:
4187
+ create++;
4188
+ if (isOverlayRecord(index.get(m[1])?.tag, index.get(m[1])?.attrs)) overlayAdd = true;
4189
+ break;
4190
+ case 9:
4191
+ create++;
4192
+ break;
4193
+ case 11:
4194
+ remove++;
4195
+ if (isOverlayRecord(prev.get(m[1])?.tag, prev.get(m[1])?.attrs)) overlayRemove = true;
4196
+ break;
4197
+ case 10:
4198
+ move++;
4199
+ break;
4200
+ case 12:
4201
+ case 13:
4202
+ if (!newIds.has(m[1])) attr++;
4203
+ break;
4204
+ case 14:
4205
+ if (!newIds.has(m[1])) text++;
4206
+ break;
4207
+ }
4208
+ }
4209
+ const activity = { t, create, remove, move, attr, text, overlayAdd, overlayRemove };
4210
+ this.analyzer.recordActivity(activity);
4211
+ this.index = index;
4212
+ this.drainPending(t);
4213
+ }
4214
+ tap(input, t) {
4215
+ const [id] = this.mirror.getId(input.key);
4216
+ const selector = computeSelector(input.path);
4217
+ const label = computeLabel(input.attrs, input.text);
4218
+ const nx = Math.round(input.normX * 1e4);
4219
+ const ny = Math.round(input.normY * 1e4);
4220
+ this.emit(t, MobileTap(id, input.hesitationMs ?? 0, label, selector, nx, ny));
4221
+ this.analyzer.recordTap({ t, nodeId: id, selector });
4222
+ this.pending.push({ t, nodeId: id, selector });
4223
+ this.drainPending(t);
4224
+ }
4225
+ screenView(name, prevName, t) {
4226
+ this.emit(t, ScreenView(name, prevName));
4227
+ this.analyzer.recordScreenView(t);
4228
+ this.drainPending(t);
4229
+ }
4230
+ /** Evaluate any pending taps whose outcome window has closed by time `t`. */
4231
+ drainPending(t) {
4232
+ const ready = this.pending.filter((p) => t >= p.t + OUTCOME_WINDOW_MS);
4233
+ if (!ready.length) return;
4234
+ this.pending = this.pending.filter((p) => t < p.t + OUTCOME_WINDOW_MS);
4235
+ for (const p of ready) this.evaluateTap(p);
4236
+ }
4237
+ evaluateTap(p) {
4238
+ const at = p.t + OUTCOME_WINDOW_MS;
4239
+ const v = this.analyzer.evaluate(p);
4240
+ if (v.frustration) {
4241
+ this.emit(at, Frustration(v.frustration.kind, p.nodeId, p.selector, v.frustration.count));
4242
+ }
4243
+ this.emit(at, TapOutcome(p.nodeId, v.outcome.kind, v.outcome.label, v.outcome.mutationCount));
4244
+ }
4245
+ /** Explicit clock advance (adapter ticker) to close outcome windows even when
4246
+ * no commit/tap arrives. */
4247
+ advance(t) {
4248
+ this.drainPending(t);
4249
+ }
4250
+ /** Flush all remaining pending taps (session end). `_t` is the session-end
4251
+ * clock, accepted for API symmetry with advance(); evaluation uses each tap's
4252
+ * own outcome window. */
4253
+ finalize(_t) {
4254
+ const ready = this.pending;
4255
+ this.pending = [];
4256
+ for (const p of ready) this.evaluateTap(p);
4257
+ this.stream.sort((a, b) => a.t - b.t);
4258
+ }
4259
+ getStream() {
4260
+ return [...this.stream].sort((a, b) => a.t - b.t);
4261
+ }
4262
+ get viewportSize() {
4263
+ return this.viewport;
4264
+ }
4265
+ };
4266
+
4267
+ // src/react-native/structural/install-hook.ts
4268
+ var g = globalThis;
4269
+ var hookState = g.__SITEPONG_STRUCTURAL_HOOK_STATE__ ?? (g.__SITEPONG_STRUCTURAL_HOOK_STATE__ = {
4270
+ latestRoot: null,
4271
+ dirty: false
4272
+ });
4273
+ function record(root) {
4274
+ hookState.latestRoot = root;
4275
+ hookState.dirty = true;
4276
+ }
4277
+ function installStructuralHook() {
4278
+ if (g.__SITEPONG_STRUCTURAL_HOOK_INSTALLED__) return;
4279
+ const existing = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
4280
+ if (existing) {
4281
+ const prevRaw = existing.onCommitFiberRoot;
4282
+ const prev = typeof prevRaw === "function" ? prevRaw.bind(existing) : null;
4283
+ existing.onCommitFiberRoot = (id, root, ...rest) => {
4284
+ record(root);
4285
+ return prev ? prev(id, root, ...rest) : void 0;
4286
+ };
4287
+ } else {
4288
+ const renderers = /* @__PURE__ */ new Map();
4289
+ g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
4290
+ renderers,
4291
+ supportsFiber: true,
4292
+ inject(renderer) {
4293
+ const id = renderers.size + 1;
4294
+ renderers.set(id, renderer);
4295
+ return id;
4296
+ },
4297
+ onCommitFiberRoot(_id, root) {
4298
+ record(root);
4299
+ },
4300
+ onCommitFiberUnmount() {
4301
+ },
4302
+ onScheduleFiberRoot() {
4303
+ },
4304
+ on() {
4305
+ },
4306
+ off() {
4307
+ },
4308
+ sub() {
4309
+ return () => {
4310
+ };
4311
+ },
4312
+ checkDCE() {
4313
+ }
4314
+ };
4315
+ }
4316
+ g.__SITEPONG_STRUCTURAL_HOOK_INSTALLED__ = true;
4317
+ }
4318
+ var HostComponent = 5;
4319
+ var HostText = 6;
4320
+ function mapTag(type) {
4321
+ if (typeof type !== "string") return "View";
4322
+ const t = type;
4323
+ if (/TextInput|TextField/i.test(t)) return "TextInput";
4324
+ if (/Image/i.test(t)) return "Image";
4325
+ if (/ScrollView/i.test(t)) return "ScrollView";
4326
+ if (/Modal/i.test(t)) return "Modal";
4327
+ if (/Text|RawText/i.test(t)) return "Text";
4328
+ if (/Pressable|Button|Touchable/i.test(t)) return "Pressable";
4329
+ return "View";
4330
+ }
4331
+ function nativeTagOf(stateNode) {
4332
+ if (!stateNode) return void 0;
4333
+ return stateNode.__nativeTag ?? stateNode._nativeTag ?? stateNode.canonical?.nativeTag ?? stateNode.canonical?._nativeTag ?? (typeof stateNode === "number" ? stateNode : void 0);
4334
+ }
4335
+ var STYLE_KEYS = /* @__PURE__ */ new Set([
4336
+ "flex",
4337
+ "flexDirection",
4338
+ "flexGrow",
4339
+ "flexShrink",
4340
+ "flexWrap",
4341
+ "alignItems",
4342
+ "alignSelf",
4343
+ "justifyContent",
4344
+ "width",
4345
+ "height",
4346
+ "minWidth",
4347
+ "minHeight",
4348
+ "maxWidth",
4349
+ "maxHeight",
4350
+ "margin",
4351
+ "marginTop",
4352
+ "marginBottom",
4353
+ "marginLeft",
4354
+ "marginRight",
4355
+ "marginHorizontal",
4356
+ "marginVertical",
4357
+ "padding",
4358
+ "paddingTop",
4359
+ "paddingBottom",
4360
+ "paddingLeft",
4361
+ "paddingRight",
4362
+ "paddingHorizontal",
4363
+ "paddingVertical",
4364
+ "backgroundColor",
4365
+ "borderRadius",
4366
+ "borderWidth",
4367
+ "borderColor",
4368
+ "opacity",
4369
+ "position",
4370
+ "top",
4371
+ "left",
4372
+ "right",
4373
+ "bottom",
4374
+ "color",
4375
+ "fontSize",
4376
+ "fontWeight",
4377
+ "textAlign",
4378
+ "lineHeight",
4379
+ "gap",
4380
+ "overflow",
4381
+ "shadowColor",
4382
+ "shadowOpacity",
4383
+ "shadowRadius",
4384
+ "elevation",
4385
+ "transform",
4386
+ "zIndex",
4387
+ "aspectRatio"
4388
+ ]);
4389
+ function extractStyle(style) {
4390
+ try {
4391
+ const flat = reactNative.StyleSheet.flatten(style);
4392
+ if (!flat) return void 0;
4393
+ const out = {};
4394
+ for (const k of Object.keys(flat)) if (STYLE_KEYS.has(k)) out[k] = flat[k];
4395
+ return Object.keys(out).length ? JSON.stringify(out) : void 0;
4396
+ } catch {
4397
+ return void 0;
4398
+ }
4399
+ }
4400
+ function extractAttrs(tag, props, componentName2) {
4401
+ const attrs = {};
4402
+ if (!props) return attrs;
4403
+ if (props.testID) attrs.testID = String(props.testID);
4404
+ if (props.nativeID) attrs.nativeID = String(props.nativeID);
4405
+ if (props.accessibilityLabel) attrs.accessibilityLabel = String(props.accessibilityLabel);
4406
+ if (props.accessibilityRole) attrs.role = String(props.accessibilityRole);
4407
+ if (props["aria-label"]) attrs.accessibilityLabel = String(props["aria-label"]);
4408
+ const style = extractStyle(props.style);
4409
+ if (style) attrs.style = style;
4410
+ if (tag === "Image") {
4411
+ const uri = props.source?.uri ?? (typeof props.source === "string" ? props.source : void 0);
4412
+ if (uri) attrs.src = String(uri);
4413
+ }
4414
+ if (componentName2) attrs.rnComponent = componentName2;
4415
+ if (props.wtOverlay || props.accessibilityViewIsModal) attrs.wtOverlay = "true";
4416
+ if (props.wtSensitive || props.secureTextEntry) attrs.wtSensitive = "true";
4417
+ return attrs;
4418
+ }
4419
+ function collectText(fiber) {
4420
+ let out = "";
4421
+ const visit = (f) => {
4422
+ if (!f) return;
4423
+ if (f.tag === HostText) {
4424
+ const p = f.memoizedProps;
4425
+ if (typeof p === "string") out += p;
4426
+ else if (p && typeof p.text === "string") out += p.text;
4427
+ } else if (f.tag === HostComponent && typeof f.type === "string" && /RawText|VirtualText/i.test(f.type)) {
4428
+ const p = f.memoizedProps;
4429
+ if (p && typeof p.text === "string") out += p.text;
4430
+ }
4431
+ visit(f.child);
4432
+ visit(f.sibling);
4433
+ };
4434
+ visit(fiber.child);
4435
+ return out;
4436
+ }
4437
+ function buildHostChildren(fiber, inheritedName, index, path) {
4438
+ const out = [];
4439
+ let f = fiber;
4440
+ while (f) {
4441
+ collectFrom(f, inheritedName, out, index, path);
4442
+ f = f.sibling;
4443
+ }
4444
+ return out;
4445
+ }
4446
+ function componentName(fiber) {
4447
+ const t = fiber?.type;
4448
+ if (typeof t === "function") return t.displayName || t.name || void 0;
4449
+ if (t && typeof t === "object") return t.displayName || void 0;
4450
+ return void 0;
4451
+ }
4452
+ function collectFrom(fiber, inheritedName, out, index, parentPath) {
4453
+ if (!fiber) return;
4454
+ if (fiber.tag === HostComponent) {
4455
+ const tag = mapTag(fiber.type);
4456
+ const attrs = extractAttrs(tag, fiber.memoizedProps, inheritedName);
4457
+ const stateNode = fiber.stateNode;
4458
+ const key = stateNode && typeof stateNode === "object" ? stateNode : fiber;
4459
+ const isTextLeaf = tag === "Text";
4460
+ const text = isTextLeaf ? collectText(fiber) : void 0;
4461
+ const nthOfType = out.filter((n) => n.tag === tag).length + 1;
4462
+ const myPath = [...parentPath, { tag, attrs, nthOfType }];
4463
+ const children = isTextLeaf ? [] : buildHostChildren(fiber.child, void 0, index, myPath);
4464
+ const vnode = { key, tag, attrs, children, ...text ? { text } : {} };
4465
+ out.push(vnode);
4466
+ const nt = nativeTagOf(stateNode);
4467
+ if (nt !== void 0) index.set(nt, { node: vnode, path: myPath });
4468
+ return;
4469
+ }
4470
+ const name = componentName(fiber) ?? inheritedName;
4471
+ let child = fiber.child;
4472
+ while (child) {
4473
+ collectFrom(child, name, out, index, parentPath);
4474
+ child = child.sibling;
4475
+ }
4476
+ }
4477
+ function walkFiberRoot(root) {
4478
+ const index = /* @__PURE__ */ new Map();
4479
+ const current = root?.current;
4480
+ if (!current) return { tree: null, index };
4481
+ const rootKey = root ?? current;
4482
+ const children = buildHostChildren(current.child, void 0, index, []);
4483
+ const tree = { key: rootKey, tag: "View", attrs: { style: JSON.stringify({ flex: 1 }) }, children };
4484
+ return { tree, index };
4485
+ }
4486
+
4487
+ // src/react-native/structural/capture.ts
4488
+ function genSessionId() {
4489
+ const rand = Math.floor(Math.random() * 16777215).toString(36);
4490
+ return `wt-${Date.now().toString(36)}-${rand}`;
4491
+ }
4492
+ var StructuralCapture = class {
4493
+ constructor() {
4494
+ this.recorder = new Recorder();
4495
+ this.currentIndex = /* @__PURE__ */ new Map();
4496
+ this.started = 0;
4497
+ this.sampleTimer = null;
4498
+ this.flushTimer = null;
4499
+ this.screen = "";
4500
+ this.prevScreen = "";
4501
+ this.cfg = null;
4502
+ this.sessionId = "";
4503
+ this.running = false;
4504
+ }
4505
+ now() {
4506
+ return Date.now() - this.started;
4507
+ }
4508
+ isRunning() {
4509
+ return this.running;
4510
+ }
4511
+ getSessionId() {
4512
+ return this.sessionId;
4513
+ }
4514
+ start(cfg) {
4515
+ if (this.running) return;
4516
+ installStructuralHook();
4517
+ this.cfg = cfg;
4518
+ this.sessionId = cfg.sessionId || genSessionId();
4519
+ this.started = Date.now();
4520
+ this.running = true;
4521
+ const { width, height } = reactNative.Dimensions.get("window");
4522
+ this.recorder.setViewport(Math.round(width), Math.round(height), 0);
4523
+ this.sampleTimer = setInterval(() => this.sample(), cfg.sampleIntervalMs ?? 120);
4524
+ this.flushTimer = setInterval(() => void this.flush(), cfg.flushIntervalMs ?? 4e3);
4525
+ if (cfg.debug) console.log("[structural] started", this.sessionId);
4526
+ setTimeout(() => this.sample(true), 0);
4527
+ }
4528
+ sample(force = false) {
4529
+ if (!this.running) return;
4530
+ if (!force && !hookState.dirty) return;
4531
+ hookState.dirty = false;
4532
+ const root = hookState.latestRoot;
4533
+ if (!root) return;
4534
+ try {
4535
+ const { tree, index } = walkFiberRoot(root);
4536
+ this.currentIndex = index;
4537
+ this.recorder.commit(tree, this.now());
4538
+ } catch (e) {
4539
+ if (this.cfg?.debug) console.log("[structural] sample error", String(e));
4540
+ }
4541
+ }
4542
+ setScreen(name) {
4543
+ if (!this.running || name === this.screen) return;
4544
+ this.prevScreen = this.screen;
4545
+ this.screen = name;
4546
+ this.sample(true);
4547
+ this.recorder.screenView(name, this.prevScreen, this.now());
4548
+ }
4549
+ /** Resolve a native touch to a node + emit a tap. `target` is the RN
4550
+ * nativeTag from `nativeEvent.target`. */
4551
+ handleTap(target, pageX, pageY) {
4552
+ if (!this.running) return;
4553
+ const entry = this.currentIndex.get(target);
4554
+ if (!entry) return;
4555
+ const { width, height } = reactNative.Dimensions.get("window");
4556
+ this.recorder.tap(
4557
+ {
4558
+ key: entry.node.key,
4559
+ path: entry.path,
4560
+ attrs: entry.node.attrs,
4561
+ text: entry.node.text,
4562
+ normX: pageX / (width || 1),
4563
+ normY: pageY / (height || 1)
4564
+ },
4565
+ this.now()
4566
+ );
4567
+ }
4568
+ async flush() {
4569
+ if (!this.running || !this.cfg) return;
4570
+ this.recorder.advance(this.now());
4571
+ const messages = this.recorder.getStream();
4572
+ if (messages.length === 0) return;
4573
+ const vp = this.recorder.viewportSize;
4574
+ try {
4575
+ const headers = {
4576
+ "content-type": "application/json",
4577
+ "X-API-Key": this.cfg.apiKey
4578
+ };
4579
+ if (this.cfg.projectId) headers["X-Project-ID"] = this.cfg.projectId;
4580
+ await fetch(`${this.cfg.endpoint.replace(/\/$/, "")}/api/replay-stream`, {
4581
+ method: "POST",
4582
+ headers,
4583
+ body: JSON.stringify({
4584
+ session_id: this.sessionId,
4585
+ platform: this.cfg.platform,
4586
+ viewport: { w: vp.w, h: vp.h },
4587
+ messages
4588
+ })
4589
+ });
4590
+ if (this.cfg.debug) console.log("[structural] flushed", messages.length, "messages");
4591
+ } catch (e) {
4592
+ if (this.cfg.debug) console.log("[structural] flush failed", String(e));
4593
+ }
4594
+ }
4595
+ async stop() {
4596
+ if (!this.running) return;
4597
+ this.recorder.finalize(this.now());
4598
+ if (this.sampleTimer) clearInterval(this.sampleTimer);
4599
+ if (this.flushTimer) clearInterval(this.flushTimer);
4600
+ this.sampleTimer = null;
4601
+ this.flushTimer = null;
4602
+ this.running = false;
4603
+ await this.flush();
4604
+ }
4605
+ };
4606
+ var structuralCapture = new StructuralCapture();
4607
+ var TAP_MOVE_THRESHOLD_PX = 12;
4608
+ function StructuralCaptureProvider({ children, ...cfg }) {
4609
+ React2.useEffect(() => {
4610
+ structuralCapture.start(cfg);
4611
+ return () => {
4612
+ void structuralCapture.stop();
4613
+ };
4614
+ }, []);
4615
+ const startPos = React2.useRef(null);
4616
+ const onTouchStart = (e) => {
4617
+ const { pageX, pageY } = e.nativeEvent;
4618
+ startPos.current = { x: pageX, y: pageY };
4619
+ };
4620
+ const onTouchEnd = (e) => {
4621
+ const ne = e.nativeEvent;
4622
+ const s = startPos.current;
4623
+ startPos.current = null;
4624
+ if (!s) return;
4625
+ if (Math.abs(ne.pageX - s.x) > TAP_MOVE_THRESHOLD_PX || Math.abs(ne.pageY - s.y) > TAP_MOVE_THRESHOLD_PX) {
4626
+ return;
4627
+ }
4628
+ structuralCapture.handleTap(Number(ne.target), ne.pageX, ne.pageY);
4629
+ };
4630
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, onTouchStart, onTouchEnd, children });
4631
+ }
4632
+ function useStructuralScreen(name) {
4633
+ React2.useEffect(() => {
4634
+ structuralCapture.setScreen(name);
4635
+ }, [name]);
4636
+ }
4637
+
3909
4638
  // src/react-native/sqlite-tracker.ts
3910
4639
  function now() {
3911
4640
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
@@ -4326,6 +5055,8 @@ exports.RNAutocaptureModule = RNAutocaptureModule;
4326
5055
  exports.RNPerformanceManager = RNPerformanceManager;
4327
5056
  exports.SensitiveView = SensitiveView;
4328
5057
  exports.SitePongRNProvider = SitePongRNProvider;
5058
+ exports.StructuralCapture = StructuralCapture;
5059
+ exports.StructuralCaptureProvider = StructuralCaptureProvider;
4329
5060
  exports.WatchtowerProvider = WatchtowerProvider;
4330
5061
  exports.addBreadcrumb = addBreadcrumb;
4331
5062
  exports.addSensitiveView = addSensitiveView;
@@ -4360,6 +5091,7 @@ exports.handleUniversalLink = handleUniversalLink;
4360
5091
  exports.identify = identify2;
4361
5092
  exports.initRN = initRN;
4362
5093
  exports.initSuperLinkRN = initSuperLinkRN;
5094
+ exports.installStructuralHook = installStructuralHook;
4363
5095
  exports.isInitialized = isInitialized;
4364
5096
  exports.isRemoteConfigFeatureEnabled = isRemoteConfigFeatureEnabled;
4365
5097
  exports.isScreenRecording = isScreenRecording;
@@ -4391,6 +5123,7 @@ exports.startWatchtower = startWatchtower;
4391
5123
  exports.stopScreenRecording = stopScreenRecording;
4392
5124
  exports.stopWatchtower = stopWatchtower;
4393
5125
  exports.stopWidgetSync = stopWidgetSync;
5126
+ exports.structuralCapture = structuralCapture;
4394
5127
  exports.subscribeNavigationToWatchtower = subscribeNavigationToWatchtower;
4395
5128
  exports.superlinkIdentify = identify3;
4396
5129
  exports.syncWidgets = syncWidgets;
@@ -4403,6 +5136,7 @@ exports.useRNPerformance = useRNPerformance;
4403
5136
  exports.useRemoteConfig = useRemoteConfig;
4404
5137
  exports.useScreenTrack = useScreenTrack;
4405
5138
  exports.useSitePongRNContext = useSitePongRNContext;
5139
+ exports.useStructuralScreen = useStructuralScreen;
4406
5140
  exports.waitForFlags = waitForFlags;
4407
5141
  //# sourceMappingURL=rn.js.map
4408
5142
  //# sourceMappingURL=rn.js.map