@goliapkg/sentori-react-native 1.0.0-rc.6 → 1.0.0-rc.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.
@@ -1,10 +1,12 @@
1
1
  package com.sentori
2
2
 
3
3
  import android.app.Activity
4
+ import android.graphics.drawable.BitmapDrawable
4
5
  import android.graphics.drawable.ColorDrawable
5
6
  import android.graphics.drawable.Drawable
6
7
  import android.graphics.drawable.GradientDrawable
7
- import android.graphics.drawable.RippleDrawable
8
+ import android.graphics.drawable.LayerDrawable
9
+ import android.graphics.drawable.StateListDrawable
8
10
  import android.view.View
9
11
  import android.view.ViewGroup
10
12
  import android.widget.EditText
@@ -93,9 +95,18 @@ object SentoriReplayCapture {
93
95
  totalEmptyResultTicks++
94
96
  return null
95
97
  }
96
- val root = activity.window?.decorView
98
+ // rc.8 anchor the walk at android.R.id.content, NOT
99
+ // window.decorView. decorView includes the StatusBarBackground
100
+ // and NavigationBarBackground sibling views the PhoneWindow
101
+ // injects (full display width, positioned in absolute window
102
+ // coords). Insight 2026-05-18 saw those bleed into the
103
+ // wireframe as horizontal grey bars stretching beyond the
104
+ // viewport width. Anchoring at the content FrameLayout drops
105
+ // them while keeping the app's React tree intact.
106
+ val decor = activity.window?.decorView
107
+ val root = decor?.findViewById<View>(android.R.id.content)
97
108
  if (root == null) {
98
- lastDiagPath = "decorView.null"
109
+ lastDiagPath = if (decor == null) "decorView.null" else "contentView.null"
99
110
  totalEmptyResultTicks++
100
111
  return null
101
112
  }
@@ -217,18 +228,21 @@ object SentoriReplayCapture {
217
228
  kindEmitted = true
218
229
  }
219
230
  view.background != null -> {
220
- node.put("kind", "rect")
221
- // rc.5 extract the fill colour from the
222
- // background Drawable so wireframes show the host
223
- // app's actual brand palette, not a uniform grey
224
- // grid. iOS' UIView.backgroundColor already drives
225
- // this path; the Android side had been emitting
226
- // null since v0.9.6 and the dashboard fell back to
227
- // a neutral fill for every coloured CTA. Insight
228
- // 2026-05-18 verify event made the gap visible.
229
- val color = extractDrawableColor(view.background)
230
- if (color != null && (color shr 24 and 0xff) != 0) {
231
- node.put("color", colorToHex(color))
231
+ // rc.8 — backgrounds backed by a BitmapDrawable
232
+ // (a View whose backgroundImage / drawable
233
+ // resource is a raster) emit as `image` kind so
234
+ // the dashboard renders them as a media region,
235
+ // not as a grey rect. Everything else stays
236
+ // `rect` and tries to extract a fill colour.
237
+ val bg = view.background
238
+ if (bg is BitmapDrawable) {
239
+ node.put("kind", "image")
240
+ } else {
241
+ node.put("kind", "rect")
242
+ val color = extractDrawableColor(bg)
243
+ if (color != null && (color shr 24 and 0xff) != 0) {
244
+ node.put("color", colorToHex(color))
245
+ }
232
246
  }
233
247
  kindEmitted = true
234
248
  }
@@ -256,31 +270,45 @@ object SentoriReplayCapture {
256
270
  return String.format("#%02X%02X%02X%02X", r, g, b, a)
257
271
  }
258
272
 
259
- /** rc.5 — best-effort fill-colour extraction for the View's
260
- * background Drawable. RN's `backgroundColor: '#...'` style
261
- * lands as a ColorDrawable in flat cases and a GradientDrawable
262
- * whenever the View also carries `borderRadius` or
263
- * `borderWidth`; Pressables wrap the painted child in a
264
- * RippleDrawable layer list. We cover all three. Returns
265
- * the packed ARGB int or null when nothing usable is exposed
266
- * (StateListDrawable with no current state, opaque image
267
- * drawables, etc.). */
273
+ /** rc.5 / rc.7 — best-effort fill-colour extraction for the View's
274
+ * background Drawable. We hit:
275
+ *
276
+ * - `ColorDrawable` — flat `<View style={{ backgroundColor }}>`.
277
+ * - `GradientDrawable` RN 0.73's path for backgroundColor +
278
+ * borderRadius / borderWidth.
279
+ * - Any `LayerDrawable` subclass RippleDrawable (Pressable),
280
+ * `com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable`
281
+ * (RN 0.74+ Fabric path, wraps the real
282
+ * `BackgroundDrawable` layer alongside borders / shadows), and
283
+ * any other future composite. Iterate layers, recurse.
284
+ * - **rc.7 reflective fallback** — for the BackgroundDrawable
285
+ * itself (RN 0.74+, Kotlin `internal class` so we can't
286
+ * import it from this module) and any other custom Drawable
287
+ * that follows the convention of exposing `getBackgroundColor()`
288
+ * or `getColor()`. Without this, Insight's app — which uses
289
+ * Pressables and rounded Views and so renders nearly every
290
+ * coloured surface via CompositeBackgroundDrawable —
291
+ * surfaced zero `node.color` fields on rc.5. */
268
292
  private fun extractDrawableColor(drawable: Drawable?): Int? {
269
293
  return when (drawable) {
270
294
  null -> null
271
295
  is ColorDrawable -> drawable.color
272
296
  is GradientDrawable -> {
273
- // API 24+ exposes the ColorStateList for the
274
- // `setColor()` value. RN's typical solid-colour
275
- // GradientDrawable returns a single default colour
276
- // here; gradients with multiple stops still surface
277
- // a reasonable representative colour.
278
297
  val csl = drawable.color
279
298
  csl?.defaultColor
280
299
  }
281
- is RippleDrawable -> {
282
- // Iterate the layer list; the inner painted layer
283
- // is what carries the brand colour.
300
+ is StateListDrawable -> {
301
+ // rc.8 Pressable / TouchableOpacity wrap their child
302
+ // in a StateListDrawable (default state + pressed
303
+ // state). `.current` returns the currently-applied
304
+ // state's drawable, which during a normal capture
305
+ // is the unpressed visual — exactly what we want.
306
+ // AnimatedStateListDrawable extends StateListDrawable
307
+ // so it inherits this branch.
308
+ extractDrawableColor(drawable.current)
309
+ }
310
+ is BitmapDrawable -> null
311
+ is LayerDrawable -> {
284
312
  for (i in 0 until drawable.numberOfLayers) {
285
313
  val inner = drawable.getDrawable(i)
286
314
  val c = extractDrawableColor(inner)
@@ -288,7 +316,31 @@ object SentoriReplayCapture {
288
316
  }
289
317
  null
290
318
  }
291
- else -> null
319
+ else -> extractByReflection(drawable)
320
+ }
321
+ }
322
+
323
+ /** rc.7 — read `getBackgroundColor()` / `getColor()` via Java
324
+ * reflection. Kotlin synthesizes the former from any `var
325
+ * backgroundColor: Int` declaration, so RN's internal
326
+ * `BackgroundDrawable` (which holds the actual paint colour
327
+ * behind any RN View with backgroundColor) exposes it
328
+ * automatically. Any throw / non-Int return falls through to
329
+ * null — fully best-effort. */
330
+ private fun extractByReflection(drawable: Drawable): Int? {
331
+ val cls = drawable.javaClass
332
+ for (name in arrayOf("getBackgroundColor", "getColor")) {
333
+ try {
334
+ val method = cls.getMethod(name)
335
+ val result = method.invoke(drawable)
336
+ if (result is Int) return result
337
+ } catch (_: NoSuchMethodException) {
338
+ // try next
339
+ } catch (_: Throwable) {
340
+ // some custom drawables throw on reflective access; bail
341
+ return null
342
+ }
292
343
  }
344
+ return null
293
345
  }
294
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.8",
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": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",