@goliapkg/sentori-react-native 5.1.2 → 5.1.3
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.
|
@@ -120,7 +120,8 @@ object SentoriReplayCapture {
|
|
|
120
120
|
val nodes = JSONArray()
|
|
121
121
|
val rootLoc = IntArray(2).also { root.getLocationInWindow(it) }
|
|
122
122
|
val ctx = WalkContext(rootLoc = rootLoc, maskedSet = maskedSet)
|
|
123
|
-
|
|
123
|
+
val rootClip = android.graphics.Rect(0, 0, root.width, root.height)
|
|
124
|
+
walk(root, depth = 0, parentMasked = false, ctx = ctx, clip = rootClip, nodes = nodes)
|
|
124
125
|
|
|
125
126
|
val payload = JSONObject().apply {
|
|
126
127
|
put("ts", System.currentTimeMillis())
|
|
@@ -173,6 +174,7 @@ object SentoriReplayCapture {
|
|
|
173
174
|
depth: Int,
|
|
174
175
|
parentMasked: Boolean,
|
|
175
176
|
ctx: WalkContext,
|
|
177
|
+
clip: android.graphics.Rect,
|
|
176
178
|
nodes: JSONArray,
|
|
177
179
|
) {
|
|
178
180
|
if (nodes.length() >= MAX_NODES) return
|
|
@@ -188,68 +190,94 @@ object SentoriReplayCapture {
|
|
|
188
190
|
val w = view.width
|
|
189
191
|
val h = view.height
|
|
190
192
|
|
|
193
|
+
// Insight 2026-08-01: nothing was clipped here at all — a
|
|
194
|
+
// scroll container's full content width shipped as-is and
|
|
195
|
+
// drew past the device in the replay. `clip` accumulates
|
|
196
|
+
// every clipping ancestor (clipChildren defaults to true, so
|
|
197
|
+
// scroll viewports narrow it); a node ships as its visible
|
|
198
|
+
// intersection, an invisible one not at all. This is the
|
|
199
|
+
// root-cause fix the 05-18 decorView→content anchor swap
|
|
200
|
+
// only sidestepped.
|
|
201
|
+
var childClip = clip
|
|
202
|
+
|
|
191
203
|
// Emit a node ONLY when the view has visual extent. A zero-
|
|
192
204
|
// size view contributes nothing to render but its subtree
|
|
193
|
-
// might; recurse below regardless
|
|
205
|
+
// might; recurse below regardless (and without narrowing the
|
|
206
|
+
// clip — transient zero-size Fabric wrappers do not really
|
|
207
|
+
// clip their laid-out descendants).
|
|
194
208
|
if (w > 0 && h > 0) {
|
|
195
209
|
val loc = IntArray(2)
|
|
196
210
|
view.getLocationInWindow(loc)
|
|
197
211
|
val x = loc[0] - ctx.rootLoc[0]
|
|
198
212
|
val y = loc[1] - ctx.rootLoc[1]
|
|
199
213
|
|
|
200
|
-
val
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
214
|
+
val selfRect = android.graphics.Rect(x, y, x + w, y + h)
|
|
215
|
+
val visible = android.graphics.Rect(selfRect)
|
|
216
|
+
if (!visible.intersect(clip)) {
|
|
217
|
+
// Fully outside every clipping ancestor: the view
|
|
218
|
+
// shows nothing, and if it clips its own children the
|
|
219
|
+
// whole subtree is gone with it.
|
|
220
|
+
if (view is ViewGroup && view.clipChildren) return
|
|
221
|
+
visible.setEmpty()
|
|
205
222
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
val text = view.text.toString().let { if (it.length > 200) it.substring(0, 200) else it }
|
|
216
|
-
node.put("text", text)
|
|
217
|
-
node.put("color", colorToHex(view.currentTextColor))
|
|
218
|
-
kindEmitted = true
|
|
219
|
-
}
|
|
220
|
-
view is EditText -> {
|
|
221
|
-
node.put("kind", "text")
|
|
222
|
-
val text = (view.text ?: "").toString().let { if (it.length > 200) it.substring(0, 200) else it }
|
|
223
|
-
node.put("text", text)
|
|
224
|
-
kindEmitted = true
|
|
225
|
-
}
|
|
226
|
-
view is ImageView -> {
|
|
227
|
-
node.put("kind", "image")
|
|
228
|
-
kindEmitted = true
|
|
223
|
+
if (view is ViewGroup && view.clipChildren) {
|
|
224
|
+
childClip = android.graphics.Rect(visible)
|
|
225
|
+
}
|
|
226
|
+
if (!visible.isEmpty) {
|
|
227
|
+
val node = JSONObject().apply {
|
|
228
|
+
put("x", visible.left)
|
|
229
|
+
put("y", visible.top)
|
|
230
|
+
put("w", visible.width())
|
|
231
|
+
put("h", visible.height())
|
|
229
232
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
|
|
234
|
+
var kindEmitted = false
|
|
235
|
+
when {
|
|
236
|
+
masked -> {
|
|
237
|
+
node.put("kind", "mask")
|
|
238
|
+
kindEmitted = true
|
|
239
|
+
}
|
|
240
|
+
view is TextView && !view.text.isNullOrEmpty() -> {
|
|
241
|
+
node.put("kind", "text")
|
|
242
|
+
val text = view.text.toString().let { if (it.length > 200) it.substring(0, 200) else it }
|
|
243
|
+
node.put("text", text)
|
|
244
|
+
node.put("color", colorToHex(view.currentTextColor))
|
|
245
|
+
kindEmitted = true
|
|
246
|
+
}
|
|
247
|
+
view is EditText -> {
|
|
248
|
+
node.put("kind", "text")
|
|
249
|
+
val text = (view.text ?: "").toString().let { if (it.length > 200) it.substring(0, 200) else it }
|
|
250
|
+
node.put("text", text)
|
|
251
|
+
kindEmitted = true
|
|
252
|
+
}
|
|
253
|
+
view is ImageView -> {
|
|
239
254
|
node.put("kind", "image")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
kindEmitted = true
|
|
256
|
+
}
|
|
257
|
+
view.background != null -> {
|
|
258
|
+
// rc.8 — backgrounds backed by a BitmapDrawable
|
|
259
|
+
// (a View whose backgroundImage / drawable
|
|
260
|
+
// resource is a raster) emit as `image` kind so
|
|
261
|
+
// the dashboard renders them as a media region,
|
|
262
|
+
// not as a grey rect. Everything else stays
|
|
263
|
+
// `rect` and tries to extract a fill colour.
|
|
264
|
+
val bg = view.background
|
|
265
|
+
if (bg is BitmapDrawable) {
|
|
266
|
+
node.put("kind", "image")
|
|
267
|
+
} else {
|
|
268
|
+
node.put("kind", "rect")
|
|
269
|
+
val color = extractDrawableColor(bg)
|
|
270
|
+
if (color != null && (color shr 24 and 0xff) != 0) {
|
|
271
|
+
node.put("color", colorToHex(color))
|
|
272
|
+
}
|
|
245
273
|
}
|
|
274
|
+
kindEmitted = true
|
|
246
275
|
}
|
|
247
|
-
kindEmitted = true
|
|
248
276
|
}
|
|
249
|
-
}
|
|
250
277
|
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
if (kindEmitted) {
|
|
279
|
+
nodes.put(node)
|
|
280
|
+
}
|
|
253
281
|
}
|
|
254
282
|
}
|
|
255
283
|
|
|
@@ -257,7 +285,7 @@ object SentoriReplayCapture {
|
|
|
257
285
|
// descendants (the rc.3 fix).
|
|
258
286
|
if (!masked && view is ViewGroup) {
|
|
259
287
|
for (i in 0 until view.childCount) {
|
|
260
|
-
walk(view.getChildAt(i), depth + 1, masked, ctx, nodes)
|
|
288
|
+
walk(view.getChildAt(i), depth + 1, masked, ctx, childClip, nodes)
|
|
261
289
|
}
|
|
262
290
|
}
|
|
263
291
|
}
|
|
@@ -74,6 +74,7 @@ import UIKit
|
|
|
74
74
|
parentMasked: false,
|
|
75
75
|
maskedIds: maskedIds,
|
|
76
76
|
window: window,
|
|
77
|
+
clip: window.bounds,
|
|
77
78
|
nodes: &nodes
|
|
78
79
|
)
|
|
79
80
|
lastDiagNodes = nodes.count
|
|
@@ -195,6 +196,7 @@ import UIKit
|
|
|
195
196
|
parentMasked: Bool,
|
|
196
197
|
maskedIds: Set<String>,
|
|
197
198
|
window: UIWindow,
|
|
199
|
+
clip: CGRect,
|
|
198
200
|
nodes: inout [[String: Any]]
|
|
199
201
|
) {
|
|
200
202
|
if nodes.count >= MAX_NODES { return }
|
|
@@ -207,17 +209,29 @@ import UIKit
|
|
|
207
209
|
.map { maskedIds.contains($0) } ?? false
|
|
208
210
|
let masked = parentMasked || isThisMasked
|
|
209
211
|
|
|
212
|
+
// Insight 2026-08-01: the old check culled only nodes FULLY
|
|
213
|
+
// outside the window, so a 2000pt scroll ruler shipped at
|
|
214
|
+
// 2000pt and drew past the phone. `clip` accumulates every
|
|
215
|
+
// clipping ancestor (scroll views, clipsToBounds containers)
|
|
216
|
+
// from the window down; a node ships as the part of it that
|
|
217
|
+
// is actually visible, and a subtree behind a clipping edge
|
|
218
|
+
// ships not at all. This replaces the 05-18 anchor-swap class
|
|
219
|
+
// of fix: clipping is the root cause, anchors were symptoms.
|
|
210
220
|
let frame = view.convert(view.bounds, to: window)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
let visible = frame.intersection(clip)
|
|
222
|
+
let clipsChildren = view.clipsToBounds
|
|
223
|
+
if visible.isNull || visible.isEmpty {
|
|
224
|
+
// Nothing of this view shows. Its children can still show
|
|
225
|
+
// when this view does not clip them (transform-heavy or
|
|
226
|
+
// zero-size wrappers); a clipping view ends the subtree.
|
|
227
|
+
if clipsChildren { return }
|
|
214
228
|
}
|
|
215
229
|
|
|
216
230
|
var node: [String: Any] = [
|
|
217
|
-
"x": Double(
|
|
218
|
-
"y": Double(
|
|
219
|
-
"w": Double(
|
|
220
|
-
"h": Double(
|
|
231
|
+
"x": Double(visible.origin.x),
|
|
232
|
+
"y": Double(visible.origin.y),
|
|
233
|
+
"w": Double(visible.size.width),
|
|
234
|
+
"h": Double(visible.size.height),
|
|
221
235
|
]
|
|
222
236
|
|
|
223
237
|
if masked {
|
|
@@ -239,11 +253,14 @@ import UIKit
|
|
|
239
253
|
}
|
|
240
254
|
// else: invisible container — skip emitting but recurse.
|
|
241
255
|
|
|
242
|
-
if node["kind"] != nil {
|
|
256
|
+
if node["kind"] != nil, !visible.isNull, !visible.isEmpty {
|
|
243
257
|
nodes.append(node)
|
|
244
258
|
}
|
|
245
259
|
|
|
246
260
|
if !masked {
|
|
261
|
+
// A clipping view narrows the clip for everything below
|
|
262
|
+
// it; a non-clipping one passes the inherited clip on.
|
|
263
|
+
let childClip = clipsChildren ? visible : clip
|
|
247
264
|
// Don't expose internals of masked subtrees.
|
|
248
265
|
for sub in view.subviews {
|
|
249
266
|
walk(
|
|
@@ -253,6 +270,7 @@ import UIKit
|
|
|
253
270
|
parentMasked: masked,
|
|
254
271
|
maskedIds: maskedIds,
|
|
255
272
|
window: window,
|
|
273
|
+
clip: childClip,
|
|
256
274
|
nodes: &nodes
|
|
257
275
|
)
|
|
258
276
|
}
|
package/lib/transport.js
CHANGED
|
@@ -15,7 +15,7 @@ const MAX_RETRY = 3;
|
|
|
15
15
|
const STORAGE_KEY = '@sentori/pending';
|
|
16
16
|
const MAX_PERSISTED = 1000;
|
|
17
17
|
// Pinned to package.json by a test — bump both together.
|
|
18
|
-
const SDK_VERSION = '5.1.
|
|
18
|
+
const SDK_VERSION = '5.1.3';
|
|
19
19
|
let _queue = [];
|
|
20
20
|
let _assertStats = new Map();
|
|
21
21
|
let _flushTimer = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.3",
|
|
4
4
|
"description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"author": "GOLIA K.K. <takagi@golia.jp> (https://golia.jp)",
|
package/src/transport.ts
CHANGED
|
@@ -20,7 +20,7 @@ const STORAGE_KEY = '@sentori/pending';
|
|
|
20
20
|
const MAX_PERSISTED = 1000;
|
|
21
21
|
|
|
22
22
|
// Pinned to package.json by a test — bump both together.
|
|
23
|
-
const SDK_VERSION = '5.1.
|
|
23
|
+
const SDK_VERSION = '5.1.3';
|
|
24
24
|
|
|
25
25
|
let _queue: WireEvent[] = [];
|
|
26
26
|
let _assertStats = new Map<string, AssertStat>();
|