@goliapkg/sentori-react-native 0.7.1 → 0.7.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.
- package/android/src/main/java/com/sentori/SentoriModule.kt +10 -0
- package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +60 -2
- package/ios/SentoriModule.swift +10 -0
- package/ios/SentoriScreenshotCapture.swift +76 -2
- package/lib/handlers/promise.d.ts.map +1 -1
- package/lib/handlers/promise.js +6 -2
- package/lib/handlers/promise.js.map +1 -1
- package/lib/handlers/screenshot.d.ts +3 -2
- package/lib/handlers/screenshot.d.ts.map +1 -1
- package/lib/handlers/screenshot.js +48 -86
- package/lib/handlers/screenshot.js.map +1 -1
- package/lib/index.d.ts +4 -5
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -5
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +8 -4
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js.map +1 -1
- package/lib/mask.d.ts +11 -47
- package/lib/mask.d.ts.map +1 -1
- package/lib/mask.js +27 -116
- package/lib/mask.js.map +1 -1
- package/lib/native.d.ts +15 -0
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +22 -0
- package/lib/native.js.map +1 -1
- package/package.json +4 -9
- package/src/__tests__/screenshot.test.ts +6 -8
- package/src/handlers/promise.ts +7 -3
- package/src/handlers/screenshot.ts +47 -110
- package/src/index.ts +4 -5
- package/src/init.ts +8 -4
- package/src/mask.ts +41 -0
- package/src/native.ts +35 -0
- package/src/mask.tsx +0 -150
|
@@ -28,6 +28,16 @@ class SentoriModule : Module() {
|
|
|
28
28
|
SentoriCrashHandler.consumePending()
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// v0.7.3 — JS-triggered screenshot path with consumer-supplied
|
|
32
|
+
// mask IDs. JS owns the registry of `nativeID`s to redact;
|
|
33
|
+
// native walks the view tree and paints black rectangles in
|
|
34
|
+
// the rendered bitmap. Returns `null` when no activity / API
|
|
35
|
+
// < 24 / capture timed out. Replaces the previous
|
|
36
|
+
// `react-native-view-shot` peer-dep path.
|
|
37
|
+
AsyncFunction("captureScreenshotWithMask") { maskedIds: List<String> ->
|
|
38
|
+
SentoriScreenshotCapture.captureScreenshotWithMask(maskedIds)
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Watchdog is opt-in from JS so the host app picks the
|
|
32
42
|
// trade-off — stricter detection vs noise from the Metro
|
|
33
43
|
// debugger pausing the main thread. Pass `force: true` to
|
|
@@ -4,6 +4,8 @@ import android.app.Activity
|
|
|
4
4
|
import android.app.Application
|
|
5
5
|
import android.graphics.Bitmap
|
|
6
6
|
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Paint
|
|
7
9
|
import android.os.Build
|
|
8
10
|
import android.os.Bundle
|
|
9
11
|
import android.os.Handler
|
|
@@ -103,16 +105,30 @@ object SentoriScreenshotCapture {
|
|
|
103
105
|
val activity = lastActivity?.get() ?: return null
|
|
104
106
|
val window = activity.window ?: return null
|
|
105
107
|
val out = mutableMapOf<String, Any>()
|
|
106
|
-
captureScreen(window)?.let { (base64, mediaType) ->
|
|
108
|
+
captureScreen(window, emptySet())?.let { (base64, mediaType) ->
|
|
107
109
|
out["screenshot"] = mapOf("base64" to base64, "mediaType" to mediaType)
|
|
108
110
|
}
|
|
109
111
|
out["viewTree"] = walkTree(window.decorView)
|
|
110
112
|
return if (out.isEmpty()) null else out
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/// v0.7.3 — JS-triggered screenshot path with consumer-supplied
|
|
116
|
+
/// mask IDs. Returns `{ base64, mediaType }` or `null`; matches
|
|
117
|
+
/// the iOS bridge contract. Native walks the view tree by
|
|
118
|
+
/// `View.tag` (RN bridges JS `nativeID` to the default String tag
|
|
119
|
+
/// on the underlying View) and paints black rectangles over each
|
|
120
|
+
/// masked subview's frame on the captured bitmap.
|
|
121
|
+
@JvmStatic
|
|
122
|
+
fun captureScreenshotWithMask(maskedIds: List<String>): Map<String, String>? {
|
|
123
|
+
val activity = lastActivity?.get() ?: return null
|
|
124
|
+
val window = activity.window ?: return null
|
|
125
|
+
val (base64, mediaType) = captureScreen(window, maskedIds.toHashSet()) ?: return null
|
|
126
|
+
return mapOf("base64" to base64, "mediaType" to mediaType)
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
// ── screenshot ────────────────────────────────────────────────
|
|
114
130
|
|
|
115
|
-
private fun captureScreen(window: Window): Pair<String, String>? {
|
|
131
|
+
private fun captureScreen(window: Window, maskedIds: Set<String>): Pair<String, String>? {
|
|
116
132
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
117
133
|
// PixelCopy is API 24+. Older Android: fall back to a
|
|
118
134
|
// `View.draw(Canvas)` path that *must* run on main and
|
|
@@ -166,6 +182,28 @@ object SentoriScreenshotCapture {
|
|
|
166
182
|
}
|
|
167
183
|
if (!success) return null
|
|
168
184
|
|
|
185
|
+
// v0.7.3 — paint black rectangles over masked subviews on the
|
|
186
|
+
// already-captured bitmap. We get window-relative coordinates
|
|
187
|
+
// from `getLocationInWindow` (respects parent transforms) and
|
|
188
|
+
// scale them down to the output bitmap size.
|
|
189
|
+
if (maskedIds.isNotEmpty()) {
|
|
190
|
+
val regions = findMaskedViews(decor, maskedIds)
|
|
191
|
+
if (regions.isNotEmpty()) {
|
|
192
|
+
val canvas = Canvas(bitmap)
|
|
193
|
+
val paint = Paint().apply { color = Color.BLACK }
|
|
194
|
+
val rootLoc = IntArray(2).also { decor.getLocationInWindow(it) }
|
|
195
|
+
val tmp = IntArray(2)
|
|
196
|
+
for (v in regions) {
|
|
197
|
+
v.getLocationInWindow(tmp)
|
|
198
|
+
val x = (tmp[0] - rootLoc[0]) * scale
|
|
199
|
+
val y = (tmp[1] - rootLoc[1]) * scale
|
|
200
|
+
val rw = v.width * scale
|
|
201
|
+
val rh = v.height * scale
|
|
202
|
+
canvas.drawRect(x, y, x + rw, y + rh, paint)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
169
207
|
val baos = ByteArrayOutputStream(64 * 1024)
|
|
170
208
|
val mediaType: String
|
|
171
209
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
@@ -181,6 +219,26 @@ object SentoriScreenshotCapture {
|
|
|
181
219
|
return Pair(base64, mediaType)
|
|
182
220
|
}
|
|
183
221
|
|
|
222
|
+
/// Depth-first walk that stops descending once a masked subtree
|
|
223
|
+
/// is hit. RN bridges JS `nativeID` to `View.setTag(Object)` with
|
|
224
|
+
/// a String value — that's why we cast to `String` rather than
|
|
225
|
+
/// looking at the int resource-id tag space.
|
|
226
|
+
private fun findMaskedViews(root: View, ids: Set<String>): List<View> {
|
|
227
|
+
val out = mutableListOf<View>()
|
|
228
|
+
fun walk(v: View) {
|
|
229
|
+
val tag = v.tag as? String
|
|
230
|
+
if (tag != null && ids.contains(tag)) {
|
|
231
|
+
out.add(v)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
if (v is ViewGroup) {
|
|
235
|
+
for (i in 0 until v.childCount) walk(v.getChildAt(i))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
walk(root)
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
241
|
+
|
|
184
242
|
// ── view tree ─────────────────────────────────────────────────
|
|
185
243
|
|
|
186
244
|
/** Synchronously walk the view hierarchy from `root`. Safe to call
|
package/ios/SentoriModule.swift
CHANGED
|
@@ -24,6 +24,16 @@ public class SentoriModule: Module {
|
|
|
24
24
|
return SentoriCrashHandler.consumePending()
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// v0.7.3 — JS-triggered screenshot path with consumer-supplied
|
|
28
|
+
// mask IDs. JS owns the registry of `nativeID`s to redact;
|
|
29
|
+
// native walks the view tree and paints black rectangles in
|
|
30
|
+
// the rendered bitmap. Returns `nil` (resolves to `null` in
|
|
31
|
+
// JS) when there's no key window or render fails. Replaces
|
|
32
|
+
// the previous `react-native-view-shot` peer-dep path.
|
|
33
|
+
AsyncFunction("captureScreenshotWithMask") { (maskedIds: [String]) -> [String: String]? in
|
|
34
|
+
return SentoriScreenshotCapture.captureScreenshotWithMask(maskedIds: maskedIds)
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// Phase 22 sub-E: opt-in iOS hang watchdog. Same JS function
|
|
28
38
|
// name as Android (sub-D) so the host app calls
|
|
29
39
|
// `startAnrWatchdog(...)` once, both platforms react.
|
|
@@ -61,6 +61,37 @@ import UIKit
|
|
|
61
61
|
return result
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/// v0.7.3 — JS-triggered screenshot path. Returns just
|
|
65
|
+
/// `{ base64, mediaType }`; view tree not needed (errors that
|
|
66
|
+
/// reach here are non-fatal and surface the tree via the
|
|
67
|
+
/// breadcrumb / stack pipeline already). When `maskedIds` is
|
|
68
|
+
/// non-empty we walk the view hierarchy by
|
|
69
|
+
/// `accessibilityIdentifier` (RN bridges `nativeID` to this on
|
|
70
|
+
/// iOS) and paint a black rectangle over each subview's frame in
|
|
71
|
+
/// the captured bitmap. Called from `Sentori.captureScreenshotWithMask`
|
|
72
|
+
/// in the Expo Module bridge.
|
|
73
|
+
@objc public static func captureScreenshotWithMask(maskedIds: [String]) -> [String: String]? {
|
|
74
|
+
if Thread.isMainThread {
|
|
75
|
+
return captureWithMaskSync(maskedIds: maskedIds)
|
|
76
|
+
}
|
|
77
|
+
var result: [String: String]?
|
|
78
|
+
DispatchQueue.main.sync {
|
|
79
|
+
result = captureWithMaskSync(maskedIds: maskedIds)
|
|
80
|
+
}
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static func captureWithMaskSync(maskedIds: [String]) -> [String: String]? {
|
|
85
|
+
guard let window = keyWindow() else { return nil }
|
|
86
|
+
guard let jpeg = renderJpegBase64(window: window, maskedIds: Set(maskedIds)) else {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
"base64": jpeg,
|
|
91
|
+
"mediaType": "image/jpeg",
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
// MARK: - Internals
|
|
65
96
|
|
|
66
97
|
private static func captureSync() -> [String: Any]? {
|
|
@@ -92,7 +123,10 @@ import UIKit
|
|
|
92
123
|
return UIApplication.shared.windows.first
|
|
93
124
|
}
|
|
94
125
|
|
|
95
|
-
private static func renderJpegBase64(
|
|
126
|
+
private static func renderJpegBase64(
|
|
127
|
+
window: UIWindow,
|
|
128
|
+
maskedIds: Set<String> = []
|
|
129
|
+
) -> String? {
|
|
96
130
|
let bounds = window.bounds
|
|
97
131
|
let longEdge = max(bounds.width, bounds.height)
|
|
98
132
|
let scale: CGFloat = longEdge > maxLongEdgePx ? maxLongEdgePx / longEdge : 1.0
|
|
@@ -103,11 +137,32 @@ import UIKit
|
|
|
103
137
|
format.scale = 1.0
|
|
104
138
|
format.opaque = true
|
|
105
139
|
let renderer = UIGraphicsImageRenderer(size: outSize, format: format)
|
|
106
|
-
let image = renderer.image {
|
|
140
|
+
let image = renderer.image { ctx in
|
|
107
141
|
window.drawHierarchy(
|
|
108
142
|
in: CGRect(origin: .zero, size: outSize),
|
|
109
143
|
afterScreenUpdates: false
|
|
110
144
|
)
|
|
145
|
+
// v0.7.3 — paint a black rectangle over every masked
|
|
146
|
+
// subview's frame, in the same render pass so we don't
|
|
147
|
+
// pay for a second bitmap allocation. `convert(_,to:)`
|
|
148
|
+
// handles transforms and nested coordinate spaces; the
|
|
149
|
+
// scale factor maps window-points to output-pixels.
|
|
150
|
+
if !maskedIds.isEmpty {
|
|
151
|
+
let regions = findMaskedSubviews(rootView: window, ids: maskedIds)
|
|
152
|
+
if !regions.isEmpty {
|
|
153
|
+
UIColor.black.setFill()
|
|
154
|
+
for v in regions {
|
|
155
|
+
let rect = v.convert(v.bounds, to: window)
|
|
156
|
+
let scaled = CGRect(
|
|
157
|
+
x: rect.origin.x * scale,
|
|
158
|
+
y: rect.origin.y * scale,
|
|
159
|
+
width: rect.size.width * scale,
|
|
160
|
+
height: rect.size.height * scale
|
|
161
|
+
)
|
|
162
|
+
ctx.fill(scaled)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
111
166
|
}
|
|
112
167
|
guard let data = image.jpegData(compressionQuality: jpegQuality) else {
|
|
113
168
|
return nil
|
|
@@ -115,6 +170,25 @@ import UIKit
|
|
|
115
170
|
return data.base64EncodedString()
|
|
116
171
|
}
|
|
117
172
|
|
|
173
|
+
/// Depth-first walk that stops descending once a masked subtree
|
|
174
|
+
/// is hit — the entire region is being blacked out, no need to
|
|
175
|
+
/// look at children for a second match.
|
|
176
|
+
private static func findMaskedSubviews(
|
|
177
|
+
rootView: UIView,
|
|
178
|
+
ids: Set<String>
|
|
179
|
+
) -> [UIView] {
|
|
180
|
+
var found: [UIView] = []
|
|
181
|
+
func walk(_ v: UIView) {
|
|
182
|
+
if let id = v.accessibilityIdentifier, ids.contains(id) {
|
|
183
|
+
found.append(v)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
for sub in v.subviews { walk(sub) }
|
|
187
|
+
}
|
|
188
|
+
walk(rootView)
|
|
189
|
+
return found
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
private static func walkTree(root: UIView) -> [String: Any] {
|
|
119
193
|
var nodes: [String: Any] = [:]
|
|
120
194
|
var counter = 0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/handlers/promise.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/handlers/promise.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,qBAAqB,QAAO,IA0BxC,CAAC"}
|
package/lib/handlers/promise.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coerceError } from '@goliapkg/sentori-core';
|
|
1
2
|
import { captureError } from '../capture';
|
|
2
3
|
let _installed = false;
|
|
3
4
|
export const installPromiseHandler = () => {
|
|
@@ -11,8 +12,11 @@ export const installPromiseHandler = () => {
|
|
|
11
12
|
allRejections: true,
|
|
12
13
|
onUnhandled: (_id, rejection) => {
|
|
13
14
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// `coerceError` keeps the actual rejection visible. JS code
|
|
16
|
+
// routinely rejects with plain objects (`Promise.reject({code})`),
|
|
17
|
+
// which would otherwise collapse to the literal text
|
|
18
|
+
// `[object Object]` in the dashboard.
|
|
19
|
+
captureError(coerceError(rejection));
|
|
16
20
|
}
|
|
17
21
|
catch {
|
|
18
22
|
// never throw
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promise.js","sourceRoot":"","sources":["../../src/handlers/promise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAW1C,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAS,EAAE;IAC9C,IAAI,UAAU;QAAE,OAAO;IAEvB,MAAM,MAAM,GAAI,UAAsD;SACnE,cAAc,CAAC;IAClB,IAAI,MAAM,EAAE,6BAA6B,EAAE,CAAC;QAC1C,UAAU,GAAG,IAAI,CAAC;QAClB,MAAM,CAAC,6BAA6B,CAAC;YACnC,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;gBAC9B,IAAI,CAAC;oBACH,
|
|
1
|
+
{"version":3,"file":"promise.js","sourceRoot":"","sources":["../../src/handlers/promise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAW1C,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAS,EAAE;IAC9C,IAAI,UAAU;QAAE,OAAO;IAEvB,MAAM,MAAM,GAAI,UAAsD;SACnE,cAAc,CAAC;IAClB,IAAI,MAAM,EAAE,6BAA6B,EAAE,CAAC;QAC1C,UAAU,GAAG,IAAI,CAAC;QAClB,MAAM,CAAC,6BAA6B,CAAC;YACnC,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;gBAC9B,IAAI,CAAC;oBACH,4DAA4D;oBAC5D,mEAAmE;oBACnE,qDAAqD;oBACrD,sCAAsC;oBACtC,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;gBACvC,CAAC;gBAAC,MAAM,CAAC;oBACP,cAAc;gBAChB,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,wEAAwE;IACxE,mFAAmF;AACrF,CAAC,CAAC"}
|
|
@@ -5,8 +5,9 @@ export type ScreenshotBlob = {
|
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Take one screenshot, yielding the JS thread first. Returns null on
|
|
8
|
-
* any error (
|
|
9
|
-
* Caller is responsible for opt-in checks
|
|
8
|
+
* any error (no native module bound, native side refused, capture
|
|
9
|
+
* timed out, etc.). Caller is responsible for opt-in checks
|
|
10
|
+
* (`config.screenshotsEnabled`).
|
|
10
11
|
*/
|
|
11
12
|
export declare function captureScreenshot(): Promise<ScreenshotBlob | null>;
|
|
12
13
|
//# sourceMappingURL=screenshot.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/handlers/screenshot.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/handlers/screenshot.ts"],"names":[],"mappings":"AA8BA,8DAA8D;AAC9D,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA0BxE"}
|
|
@@ -1,100 +1,62 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// v0.7.3 — capture a screenshot of the current view tree on
|
|
2
|
+
// `captureException`. Off-main-thread, best-effort, opt-in.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
4
|
+
// JS owns the registry of which regions to redact: the host app
|
|
5
|
+
// passes a thunk via `sentori.registerMaskQuery(() => string[])` that
|
|
6
|
+
// returns the `nativeID`s currently mounted as masked. We call it
|
|
7
|
+
// once per capture and forward the list to the native module, which
|
|
8
|
+
// renders the bitmap and paints black rectangles over the matching
|
|
9
|
+
// subviews in a single pass.
|
|
10
|
+
//
|
|
11
|
+
// History: pre-v0.7.3 went through `react-native-view-shot` (peer
|
|
12
|
+
// dep) and used a JS-side overlay-opacity trick (`<MaskRegion>` /
|
|
13
|
+
// `setMaskedNode`) to hide PII before snapshotting. That design put
|
|
14
|
+
// the SDK on the render path; a single SDK bug could break the host
|
|
15
|
+
// app's UI. v0.7.3 cuts that coupling — the SDK no longer ships
|
|
16
|
+
// React components, and the screenshot path runs entirely through
|
|
17
|
+
// the native module already used for native-crash captures.
|
|
18
|
+
//
|
|
19
|
+
// Performance:
|
|
20
|
+
// - Yield one paint via `requestAnimationFrame` before the native
|
|
21
|
+
// call so post-error UI state has committed.
|
|
22
|
+
// - 480 px on the longest edge, JPEG q=70 (iOS) / WEBP_LOSSY q=70
|
|
23
|
+
// (Android 11+). Typical payload 30-80 KB; multipart hard cap
|
|
24
|
+
// is 500 KB.
|
|
13
25
|
// - On any failure we silently return null. The error event still
|
|
14
26
|
// goes to the server; the user just doesn't see a thumbnail.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// lazily so apps that don't install it never pay the bundle cost
|
|
18
|
-
// or fail at import time. Without it, `captureScreenshot()` returns
|
|
19
|
-
// `null` immediately.
|
|
20
|
-
import { InteractionManager } from 'react-native';
|
|
21
|
-
import { engageMasks } from '../mask';
|
|
22
|
-
function loadCaptureRef() {
|
|
23
|
-
try {
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
-
const mod = require('react-native-view-shot');
|
|
26
|
-
return mod.captureRef ?? mod.default?.captureRef ?? null;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const MAX_LONG_EDGE_PX = 480;
|
|
33
|
-
const WEBP_QUALITY = 0.7;
|
|
34
|
-
const CAPTURE_TIMEOUT_MS = 1500;
|
|
27
|
+
import { getRegisteredMaskQuery } from '../mask';
|
|
28
|
+
import { captureNativeScreenshotWithMask } from '../native';
|
|
35
29
|
/**
|
|
36
30
|
* Take one screenshot, yielding the JS thread first. Returns null on
|
|
37
|
-
* any error (
|
|
38
|
-
* Caller is responsible for opt-in checks
|
|
31
|
+
* any error (no native module bound, native side refused, capture
|
|
32
|
+
* timed out, etc.). Caller is responsible for opt-in checks
|
|
33
|
+
* (`config.screenshotsEnabled`).
|
|
39
34
|
*/
|
|
40
35
|
export async function captureScreenshot() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return null;
|
|
44
|
-
// Wait for the in-flight RN interaction batch to drain. This is
|
|
45
|
-
// why screenshot capture doesn't visibly stall the user's last
|
|
46
|
-
// action — we let React commit before we ask the OS to render.
|
|
47
|
-
await new Promise((resolve) => {
|
|
48
|
-
InteractionManager.runAfterInteractions(() => resolve());
|
|
49
|
-
});
|
|
50
|
-
await new Promise((resolve) => {
|
|
51
|
-
requestAnimationFrame(() => resolve());
|
|
52
|
-
});
|
|
53
|
-
// Phase 48 sub-B — flip every registered MaskRegion overlay to
|
|
54
|
-
// opacity 1 (black covers the children) and every imperative
|
|
55
|
-
// setMaskedNode ref to opacity 0 (subtree disappears). Held for
|
|
56
|
-
// exactly one frame's worth of capture, then restored.
|
|
57
|
-
const restoreMasks = engageMasks();
|
|
58
|
-
// Yield one more frame so the overlay paint reaches the screen
|
|
59
|
-
// before captureRef snapshots. Without this the overlay opacity
|
|
60
|
-
// change is queued but the screenshotter may see the previous
|
|
61
|
-
// frame.
|
|
36
|
+
// Yield one paint frame so the post-error UI has committed before
|
|
37
|
+
// we ask the OS to snapshot it.
|
|
62
38
|
await new Promise((resolve) => {
|
|
63
39
|
requestAnimationFrame(() => resolve());
|
|
64
40
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// RN minimum we support has it everywhere.
|
|
81
|
-
return { base64, mediaType: 'image/jpeg' };
|
|
41
|
+
// Read the consumer-supplied mask query once per capture. If
|
|
42
|
+
// the host never called `registerMaskQuery`, no mask is applied
|
|
43
|
+
// and the full screenshot ships — sane default: SDK does nothing
|
|
44
|
+
// unless told to.
|
|
45
|
+
const query = getRegisteredMaskQuery();
|
|
46
|
+
let maskedIds = [];
|
|
47
|
+
if (query) {
|
|
48
|
+
try {
|
|
49
|
+
maskedIds = query();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A throwing query is the host's bug, not ours; skip mask
|
|
53
|
+
// rather than skip the screenshot.
|
|
54
|
+
maskedIds = [];
|
|
55
|
+
}
|
|
82
56
|
}
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
const result = await captureNativeScreenshotWithMask(maskedIds);
|
|
58
|
+
if (!result)
|
|
85
59
|
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function withTimeout(p, ms) {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
const t = setTimeout(() => resolve(null), ms);
|
|
91
|
-
p.then((v) => {
|
|
92
|
-
clearTimeout(t);
|
|
93
|
-
resolve(v);
|
|
94
|
-
}, () => {
|
|
95
|
-
clearTimeout(t);
|
|
96
|
-
resolve(null);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
60
|
+
return { base64: result.base64, mediaType: result.mediaType };
|
|
99
61
|
}
|
|
100
62
|
//# sourceMappingURL=screenshot.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/handlers/screenshot.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/handlers/screenshot.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,4DAA4D;AAC5D,EAAE;AACF,gEAAgE;AAChE,sEAAsE;AACtE,kEAAkE;AAClE,oEAAoE;AACpE,mEAAmE;AACnE,6BAA6B;AAC7B,EAAE;AACF,kEAAkE;AAClE,kEAAkE;AAClE,oEAAoE;AACpE,oEAAoE;AACpE,gEAAgE;AAChE,kEAAkE;AAClE,4DAA4D;AAC5D,EAAE;AACF,eAAe;AACf,oEAAoE;AACpE,iDAAiD;AACjD,oEAAoE;AACpE,kEAAkE;AAClE,iBAAiB;AACjB,oEAAoE;AACpE,iEAAiE;AAEjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAQ5D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,kEAAkE;IAClE,gCAAgC;IAChC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,qBAAqB,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,6DAA6D;IAC7D,gEAAgE;IAChE,iEAAiE;IACjE,kBAAkB;IAClB,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,IAAI,SAAS,GAAa,EAAE,CAAC;IAC7B,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,SAAS,GAAG,KAAK,EAAE,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;YAC1D,mCAAmC;YACnC,SAAS,GAAG,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,+BAA+B,CAAC,SAAS,CAAC,CAAC;IAChE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;AAChE,CAAC"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorBoundary } from './error-boundary';
|
|
2
|
-
import {
|
|
2
|
+
import { clearMaskQuery, registerMaskQuery } from './mask';
|
|
3
3
|
export declare const sentori: {
|
|
4
4
|
init: (options: import("./init").InitOptions) => void;
|
|
5
5
|
addBreadcrumb: (input: import("./breadcrumbs").AddBreadcrumbInput) => void;
|
|
@@ -9,9 +9,8 @@ export declare const sentori: {
|
|
|
9
9
|
captureException: (error: Error, extras?: import("./capture").CaptureExtras) => void;
|
|
10
10
|
captureStep: (label: string, opts?: Partial<import("@goliapkg/sentori-core").TrailStep>) => void;
|
|
11
11
|
ErrorBoundary: typeof ErrorBoundary;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
unsetMaskedNode: typeof unsetMaskedNode;
|
|
12
|
+
registerMaskQuery: typeof registerMaskQuery;
|
|
13
|
+
clearMaskQuery: typeof clearMaskQuery;
|
|
15
14
|
startSession: () => void;
|
|
16
15
|
endSession: (status?: "exited") => void;
|
|
17
16
|
markSessionCrashed: () => void;
|
|
@@ -21,7 +20,7 @@ export { init, init as initSentori } from './init';
|
|
|
21
20
|
export { addBreadcrumb } from './breadcrumbs';
|
|
22
21
|
export { captureError, captureException, captureStep, getUser, setUser, } from './capture';
|
|
23
22
|
export { ErrorBoundary } from './error-boundary';
|
|
24
|
-
export {
|
|
23
|
+
export { clearMaskQuery, registerMaskQuery } from './mask';
|
|
25
24
|
export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
|
|
26
25
|
export { endSession, markSessionCrashed, startSession, } from './session-tracker';
|
|
27
26
|
export { type NavigationRefLike, useTraceNavigation } from './navigation';
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAO3D,eAAO,MAAM,OAAO;;;;;;;;;;;;;;CAcnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAE1E,YAAY,EACV,KAAK,EACL,YAAY,EACZ,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,SAAS,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { init } from './init';
|
|
|
2
2
|
import { addBreadcrumb } from './breadcrumbs';
|
|
3
3
|
import { setUser, getUser, captureError, captureException, captureStep } from './capture';
|
|
4
4
|
import { ErrorBoundary } from './error-boundary';
|
|
5
|
-
import {
|
|
5
|
+
import { clearMaskQuery, registerMaskQuery } from './mask';
|
|
6
6
|
import { endSession, markSessionCrashed, startSession, } from './session-tracker';
|
|
7
7
|
export const sentori = {
|
|
8
8
|
init,
|
|
@@ -13,9 +13,8 @@ export const sentori = {
|
|
|
13
13
|
captureException,
|
|
14
14
|
captureStep,
|
|
15
15
|
ErrorBoundary,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
unsetMaskedNode,
|
|
16
|
+
registerMaskQuery,
|
|
17
|
+
clearMaskQuery,
|
|
19
18
|
startSession,
|
|
20
19
|
endSession,
|
|
21
20
|
markSessionCrashed,
|
|
@@ -25,7 +24,7 @@ export { init, init as initSentori } from './init';
|
|
|
25
24
|
export { addBreadcrumb } from './breadcrumbs';
|
|
26
25
|
export { captureError, captureException, captureStep, getUser, setUser, } from './capture';
|
|
27
26
|
export { ErrorBoundary } from './error-boundary';
|
|
28
|
-
export {
|
|
27
|
+
export { clearMaskQuery, registerMaskQuery } from './mask';
|
|
29
28
|
export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
|
|
30
29
|
export { endSession, markSessionCrashed, startSession, } from './session-tracker';
|
|
31
30
|
export { useTraceNavigation } from './navigation';
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,iBAAiB;IACjB,cAAc;IACd,YAAY;IACZ,UAAU;IACV,kBAAkB;CACnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAA0B,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
|
package/lib/init.d.ts
CHANGED
|
@@ -17,10 +17,14 @@ export type InitOptions = {
|
|
|
17
17
|
* foreground (`AppState` → `active`), ends it on background.
|
|
18
18
|
* Drives crash-free rate. Set `false` to opt out. */
|
|
19
19
|
sessions?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
/** Capture a screenshot of the current screen on
|
|
21
|
+
* `captureException`. Opt-in. The capture runs through the
|
|
22
|
+
* bundled native module — no extra peer dep required since
|
|
23
|
+
* v0.7.3. To redact PII regions, register a mask query via
|
|
24
|
+
* `sentori.registerMaskQuery(() => string[])` and put
|
|
25
|
+
* `nativeID="..."` on the `<View>`s the SDK should black out.
|
|
26
|
+
* The image is webp q=70 / jpeg q=70 at 480 px max, < 100 KB
|
|
27
|
+
* typical. */
|
|
24
28
|
screenshot?: boolean;
|
|
25
29
|
/** Phase 46: record the last N steps (route changes, custom
|
|
26
30
|
* breadcrumbs) leading up to a crash. On `captureException`
|
package/lib/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;;;;;;;uBAOe;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB;;;6CAGqC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;IACF;;;;;gEAK4D;IAC5D,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAIF,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,IAoF3C,CAAC;AAiBF,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
package/lib/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|