@multiplayer-app/session-recorder-react-native 1.3.4 → 1.3.6
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/sessionrecordernative/SessionRecorderNativeModule.kt +193 -65
- package/ios/SessionRecorderNative.swift +218 -130
- package/lib/module/config/defaults.js +1 -1
- package/lib/module/config/defaults.js.map +1 -1
- package/lib/module/patch/fetch.js +4 -15
- package/lib/module/patch/fetch.js.map +1 -1
- package/lib/module/recorder/screenRecorder.js +24 -35
- package/lib/module/recorder/screenRecorder.js.map +1 -1
- package/lib/module/services/{screenMaskingService.js → screenRecordingService.js} +12 -12
- package/lib/module/services/screenRecordingService.js.map +1 -0
- package/lib/typescript/src/recorder/screenRecorder.d.ts +2 -6
- package/lib/typescript/src/recorder/screenRecorder.d.ts.map +1 -1
- package/lib/typescript/src/services/{screenMaskingService.d.ts → screenRecordingService.d.ts} +8 -8
- package/lib/typescript/src/services/screenRecordingService.d.ts.map +1 -0
- package/package.json +4 -3
- package/src/config/defaults.ts +1 -1
- package/src/patch/fetch.ts +4 -17
- package/src/recorder/screenRecorder.ts +30 -47
- package/src/services/{screenMaskingService.ts → screenRecordingService.ts} +16 -16
- package/lib/module/services/screenMaskingService.js.map +0 -1
- package/lib/typescript/src/services/screenMaskingService.d.ts.map +0 -1
|
@@ -52,30 +52,45 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
guard let image = screenshot else {
|
|
61
|
-
reject("CAPTURE_FAILED", "Failed to capture screen", nil)
|
|
55
|
+
// Skip screenshot if window is not visible or if an animation/scroll is in progress
|
|
56
|
+
// || self.isAnimatingTransition(window) || self.windowHasActiveAnimations(window)
|
|
57
|
+
if !window.isVisible() {
|
|
58
|
+
reject("ANIMATION_IN_PROGRESS", "Skipping screenshot - animation or transition in progress", nil)
|
|
62
59
|
return
|
|
63
60
|
}
|
|
64
61
|
|
|
62
|
+
// Integrate optional scale directly into the capture to avoid a second resize pass
|
|
63
|
+
let clampedScale = max(CGFloat(0.1), min(self.scale, 1.0))
|
|
64
|
+
let contextScale = UIScreen.main.scale * clampedScale
|
|
65
|
+
|
|
66
|
+
let rendererFormat = UIGraphicsImageRendererFormat()
|
|
67
|
+
rendererFormat.scale = contextScale
|
|
68
|
+
rendererFormat.opaque = false
|
|
69
|
+
|
|
70
|
+
let renderer = UIGraphicsImageRenderer(size: window.bounds.size, format: rendererFormat)
|
|
71
|
+
let image = renderer.image { _ in
|
|
72
|
+
window.drawHierarchy(in: window.bounds, afterScreenUpdates: false)
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
// Apply masking to sensitive elements
|
|
66
76
|
let maskedImage = self.applyMasking(to: image, in: window)
|
|
67
77
|
|
|
68
|
-
// Apply optional scaling (resolution downsample)
|
|
69
|
-
let finalImage = self.scale < 1.0 ? self.resizeImage(maskedImage, scale: self.scale) : maskedImage
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
let quality = self.imageQuality
|
|
80
|
+
let finalImageForEncoding = maskedImage
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
// Move JPEG encoding off the main thread to reduce UI stalls
|
|
83
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
84
|
+
if let data = finalImageForEncoding.jpegData(compressionQuality: quality) {
|
|
75
85
|
let base64 = data.base64EncodedString()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
resolve(base64)
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
DispatchQueue.main.async {
|
|
91
|
+
reject("ENCODING_FAILED", "Failed to encode image", nil)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
}
|
|
@@ -90,30 +105,45 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
90
105
|
return
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
guard let image = screenshot else {
|
|
99
|
-
reject("CAPTURE_FAILED", "Failed to capture screen", nil)
|
|
108
|
+
// Skip screenshot if window is not visible or if an animation/scroll is in progress
|
|
109
|
+
// || self.isAnimatingTransition(window) || self.windowHasActiveAnimations(window)
|
|
110
|
+
if !window.isVisible() {
|
|
111
|
+
reject("ANIMATION_IN_PROGRESS", "Skipping screenshot - animation or transition in progress", nil)
|
|
100
112
|
return
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
// Integrate optional scale directly into the capture to avoid a second resize pass
|
|
116
|
+
let clampedScale = max(CGFloat(0.1), min(self.scale, 1.0))
|
|
117
|
+
let contextScale = UIScreen.main.scale * clampedScale
|
|
118
|
+
|
|
119
|
+
let rendererFormat = UIGraphicsImageRendererFormat()
|
|
120
|
+
rendererFormat.scale = contextScale
|
|
121
|
+
rendererFormat.opaque = false
|
|
122
|
+
|
|
123
|
+
let renderer = UIGraphicsImageRenderer(size: window.bounds.size, format: rendererFormat)
|
|
124
|
+
let image = renderer.image { _ in
|
|
125
|
+
window.drawHierarchy(in: window.bounds, afterScreenUpdates: false)
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
// Apply masking with custom options
|
|
104
129
|
let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
|
|
105
130
|
|
|
106
|
-
// Apply optional scaling (resolution downsample)
|
|
107
|
-
let finalImage = self.scale < 1.0 ? self.resizeImage(maskedImage, scale: self.scale) : maskedImage
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
let quality = self.imageQuality
|
|
133
|
+
let finalImageForEncoding = maskedImage
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
// Move JPEG encoding off the main thread to reduce UI stalls
|
|
136
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
137
|
+
if let data = finalImageForEncoding.jpegData(compressionQuality: quality) {
|
|
138
|
+
let base64 = data.base64EncodedString()
|
|
139
|
+
DispatchQueue.main.async {
|
|
140
|
+
resolve(base64)
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
DispatchQueue.main.async {
|
|
144
|
+
reject("ENCODING_FAILED", "Failed to encode image", nil)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
117
147
|
}
|
|
118
148
|
}
|
|
119
149
|
}
|
|
@@ -221,48 +251,60 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
private func applyMaskingWithOptions(to image: UIImage, in window: UIWindow, options: NSDictionary) -> UIImage {
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
// Early exit optimization: if all masking options are false, skip masking entirely
|
|
255
|
+
if !hasAnyMaskingEnabled() {
|
|
256
|
+
return image
|
|
257
|
+
}
|
|
226
258
|
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
let rendererFormat = UIGraphicsImageRendererFormat()
|
|
260
|
+
rendererFormat.scale = image.scale
|
|
261
|
+
rendererFormat.opaque = false
|
|
229
262
|
|
|
263
|
+
let renderer = UIGraphicsImageRenderer(size: image.size, format: rendererFormat)
|
|
230
264
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
findMaskableWidgets(window, window, &maskableWidgets, &maskChildren)
|
|
265
|
+
let maskedImage = renderer.image { rendererContext in
|
|
266
|
+
let context = rendererContext.cgContext
|
|
234
267
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if frame == CGRect.zero { continue }
|
|
268
|
+
// Draw the original image
|
|
269
|
+
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
frame.origin.x.isFinite && frame.origin.y.isFinite else {
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
271
|
+
var maskableWidgets: [CGRect] = []
|
|
272
|
+
findMaskableWidgets(window, window, &maskableWidgets)
|
|
244
273
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
for frame in maskableWidgets {
|
|
275
|
+
// Skip zero rects (which indicate invalid coordinates)
|
|
276
|
+
if frame == CGRect.zero { continue }
|
|
248
277
|
|
|
249
|
-
|
|
250
|
-
|
|
278
|
+
// Validate frame dimensions before processing
|
|
279
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
280
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite else {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Clip the frame to the image bounds to avoid drawing outside context
|
|
285
|
+
let clippedFrame = frame.intersection(CGRect(origin: .zero, size: image.size))
|
|
286
|
+
if clippedFrame.isNull || clippedFrame.isEmpty { continue }
|
|
251
287
|
|
|
252
|
-
|
|
253
|
-
|
|
288
|
+
applyCleanMask(in: context, frame: clippedFrame)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
254
291
|
|
|
255
292
|
return maskedImage
|
|
256
293
|
}
|
|
257
294
|
|
|
258
|
-
|
|
295
|
+
// Check if any masking option is enabled (optimization)
|
|
296
|
+
private func hasAnyMaskingEnabled() -> Bool {
|
|
297
|
+
return maskTextInputs || maskImages || maskButtons || maskLabels || maskWebViews || maskSandboxedViews
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private func findMaskableWidgets(_ view: UIView, _ window: UIWindow, _ maskableWidgets: inout [CGRect]) {
|
|
259
301
|
// Skip hidden or transparent views
|
|
260
302
|
if !view.isVisible() {
|
|
261
303
|
return
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
// Check for UITextView (TextEditor, SwiftUI.TextEditorTextView, SwiftUI.UIKitTextView)
|
|
265
|
-
if let textView = view as? UITextView {
|
|
307
|
+
if maskTextInputs, let textView = view as? UITextView {
|
|
266
308
|
if isTextViewSensitive(textView) {
|
|
267
309
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
268
310
|
return
|
|
@@ -270,87 +312,89 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
270
312
|
}
|
|
271
313
|
|
|
272
314
|
// Check for UITextField (SwiftUI: TextField, SecureField)
|
|
273
|
-
if let textField = view as? UITextField {
|
|
315
|
+
if maskTextInputs, let textField = view as? UITextField {
|
|
274
316
|
if isTextFieldSensitive(textField) {
|
|
275
317
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
276
318
|
return
|
|
277
319
|
}
|
|
278
320
|
}
|
|
279
321
|
|
|
280
|
-
// React Native text views
|
|
281
|
-
if let reactNativeTextView = reactNativeTextView {
|
|
282
|
-
if view.isKind(of: reactNativeTextView)
|
|
322
|
+
// React Native text views - only if maskTextInputs is enabled
|
|
323
|
+
if maskTextInputs, let reactNativeTextView = reactNativeTextView {
|
|
324
|
+
if view.isKind(of: reactNativeTextView) {
|
|
283
325
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
284
326
|
return
|
|
285
327
|
}
|
|
286
328
|
}
|
|
287
329
|
|
|
288
|
-
// React Native text inputs
|
|
289
|
-
if let reactNativeTextInput = reactNativeTextInput {
|
|
290
|
-
if view.isKind(of: reactNativeTextInput)
|
|
330
|
+
// React Native text inputs - only if maskTextInputs is enabled
|
|
331
|
+
if maskTextInputs, let reactNativeTextInput = reactNativeTextInput {
|
|
332
|
+
if view.isKind(of: reactNativeTextInput) {
|
|
291
333
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
292
334
|
return
|
|
293
335
|
}
|
|
294
336
|
}
|
|
295
337
|
|
|
296
|
-
if let reactNativeTextInputView = reactNativeTextInputView {
|
|
297
|
-
if view.isKind(of: reactNativeTextInputView)
|
|
338
|
+
if maskTextInputs, let reactNativeTextInputView = reactNativeTextInputView {
|
|
339
|
+
if view.isKind(of: reactNativeTextInputView) {
|
|
298
340
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
299
341
|
return
|
|
300
342
|
}
|
|
301
343
|
}
|
|
302
344
|
|
|
303
|
-
// UIImageView (SwiftUI: Some control images like the ones in Picker view)
|
|
304
|
-
if let imageView = view as? UIImageView {
|
|
345
|
+
// UIImageView (SwiftUI: Some control images like the ones in Picker view) - only if maskImages is enabled
|
|
346
|
+
if maskImages, let imageView = view as? UIImageView {
|
|
305
347
|
if isImageViewSensitive(imageView) {
|
|
306
348
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
307
349
|
return
|
|
308
350
|
}
|
|
309
351
|
}
|
|
310
352
|
|
|
311
|
-
// React Native image views
|
|
312
|
-
if let reactNativeImageView = reactNativeImageView {
|
|
313
|
-
if view.isKind(of: reactNativeImageView)
|
|
353
|
+
// React Native image views - only if maskImages is enabled
|
|
354
|
+
if maskImages, let reactNativeImageView = reactNativeImageView {
|
|
355
|
+
if view.isKind(of: reactNativeImageView) {
|
|
314
356
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
315
357
|
return
|
|
316
358
|
}
|
|
317
359
|
}
|
|
318
360
|
|
|
319
|
-
// UILabel (Text
|
|
320
|
-
if let label = view as? UILabel {
|
|
321
|
-
if
|
|
361
|
+
// UILabel (Text) - only if maskLabels is enabled
|
|
362
|
+
if maskLabels, let label = view as? UILabel {
|
|
363
|
+
if hasText(label.text) || label.isNoCapture() {
|
|
322
364
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
323
365
|
return
|
|
324
366
|
}
|
|
325
367
|
}
|
|
326
368
|
|
|
327
|
-
// WKWebView
|
|
328
|
-
if let webView = view as? WKWebView {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return
|
|
332
|
-
}
|
|
369
|
+
// WKWebView - only if maskWebViews is enabled
|
|
370
|
+
if maskWebViews, let webView = view as? WKWebView {
|
|
371
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
372
|
+
return
|
|
333
373
|
}
|
|
334
374
|
|
|
335
|
-
// UIButton
|
|
336
|
-
if let button = view as? UIButton {
|
|
337
|
-
if
|
|
375
|
+
// UIButton - only if maskButtons is enabled
|
|
376
|
+
if maskButtons, let button = view as? UIButton {
|
|
377
|
+
if hasText(button.titleLabel?.text) || button.isNoCapture() {
|
|
338
378
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
339
379
|
return
|
|
340
380
|
}
|
|
341
381
|
}
|
|
342
382
|
|
|
343
|
-
// UISwitch (SwiftUI: Toggle)
|
|
344
|
-
if let theSwitch = view as? UISwitch {
|
|
345
|
-
|
|
383
|
+
// UISwitch (SwiftUI: Toggle) - only if maskButtons is enabled (treat as button-like)
|
|
384
|
+
if maskButtons, let theSwitch = view as? UISwitch {
|
|
385
|
+
var containsText = true
|
|
386
|
+
if #available(iOS 14.0, *) {
|
|
387
|
+
containsText = hasText(theSwitch.title)
|
|
388
|
+
}
|
|
389
|
+
if containsText || theSwitch.isNoCapture() {
|
|
346
390
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
347
391
|
return
|
|
348
392
|
}
|
|
349
393
|
}
|
|
350
394
|
|
|
351
|
-
// UIPickerView (SwiftUI: Picker with .pickerStyle(.wheel))
|
|
352
|
-
if let picker = view as? UIPickerView {
|
|
353
|
-
if
|
|
395
|
+
// UIPickerView (SwiftUI: Picker with .pickerStyle(.wheel)) - only if maskTextInputs is enabled
|
|
396
|
+
if maskTextInputs, let picker = view as? UIPickerView {
|
|
397
|
+
if !view.subviews.isEmpty {
|
|
354
398
|
maskableWidgets.append(picker.toAbsoluteRect(window))
|
|
355
399
|
return
|
|
356
400
|
}
|
|
@@ -367,73 +411,58 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
367
411
|
let hasSubViews = !view.subviews.isEmpty
|
|
368
412
|
|
|
369
413
|
// SwiftUI: Text based views like Text, Button, TextEditor
|
|
370
|
-
if
|
|
371
|
-
|
|
414
|
+
// Only check if relevant masking options are enabled
|
|
415
|
+
if swiftUITextBasedViewTypes.contains(where: view.isKind(of:)), !hasSubViews {
|
|
416
|
+
// Check if it's a text input (should be masked with maskTextInputs)
|
|
417
|
+
if maskTextInputs && view.isNoCapture() {
|
|
418
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
// Check if it's a button (should be masked with maskButtons)
|
|
422
|
+
if maskButtons && view.isNoCapture() {
|
|
372
423
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
373
424
|
return
|
|
374
425
|
}
|
|
375
426
|
}
|
|
376
427
|
|
|
377
|
-
// SwiftUI: Image based views like Image, AsyncImage
|
|
378
|
-
if swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)) {
|
|
379
|
-
if isSwiftUIImageSensitive(view)
|
|
428
|
+
// SwiftUI: Image based views like Image, AsyncImage - only if maskImages is enabled
|
|
429
|
+
if maskImages, swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)), !hasSubViews {
|
|
430
|
+
if isSwiftUIImageSensitive(view) {
|
|
380
431
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
381
432
|
return
|
|
382
433
|
}
|
|
383
434
|
}
|
|
384
435
|
|
|
385
|
-
// Generic SwiftUI types
|
|
386
|
-
if swiftUIGenericTypes.contains(where: { view.isKind(of: $0) }), !isSwiftUILayerSafe(view.layer) {
|
|
387
|
-
if
|
|
436
|
+
// Generic SwiftUI types - only check if relevant masking is enabled
|
|
437
|
+
if swiftUIGenericTypes.contains(where: { view.isKind(of: $0) }), !isSwiftUILayerSafe(view.layer), !hasSubViews {
|
|
438
|
+
if (maskTextInputs || maskButtons) && view.isNoCapture() {
|
|
388
439
|
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
389
440
|
return
|
|
390
441
|
}
|
|
391
442
|
}
|
|
392
443
|
|
|
393
|
-
// Recursively check subviews
|
|
444
|
+
// Recursively check subviews only if we still need to check for more widgets
|
|
445
|
+
// Early exit optimization: if we've found all we need, don't recurse
|
|
394
446
|
if !view.subviews.isEmpty {
|
|
395
447
|
for child in view.subviews {
|
|
396
448
|
if !child.isVisible() {
|
|
397
449
|
continue
|
|
398
450
|
}
|
|
399
|
-
findMaskableWidgets(child, window, &maskableWidgets
|
|
451
|
+
findMaskableWidgets(child, window, &maskableWidgets)
|
|
400
452
|
}
|
|
401
453
|
}
|
|
402
|
-
maskChildren = false
|
|
403
454
|
}
|
|
404
455
|
|
|
405
456
|
// MARK: - Sensitive Content Detection Methods
|
|
406
457
|
|
|
407
|
-
private func isAnyInputSensitive(_ view: UIView) -> Bool {
|
|
408
|
-
return isTextInputSensitive(view) || maskImages
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private func isTextInputSensitive(_ view: UIView) -> Bool {
|
|
412
|
-
return maskTextInputs || view.isNoCapture()
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
private func isLabelSensitive(_ view: UILabel) -> Bool {
|
|
416
|
-
return isTextInputSensitive(view) && hasText(view.text)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private func isButtonSensitive(_ view: UIButton) -> Bool {
|
|
420
|
-
return isTextInputSensitive(view) && hasText(view.titleLabel?.text)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
458
|
private func isTextViewSensitive(_ view: UITextView) -> Bool {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
private func isSwitchSensitive(_ view: UISwitch) -> Bool {
|
|
428
|
-
var containsText = true
|
|
429
|
-
if #available(iOS 14.0, *) {
|
|
430
|
-
containsText = hasText(view.title)
|
|
431
|
-
}
|
|
432
|
-
return isTextInputSensitive(view) && containsText
|
|
459
|
+
// Only check if maskTextInputs is enabled, or if view is explicitly marked as sensitive
|
|
460
|
+
return (maskTextInputs || view.isSensitiveText()) && hasText(view.text)
|
|
433
461
|
}
|
|
434
462
|
|
|
435
463
|
private func isTextFieldSensitive(_ view: UITextField) -> Bool {
|
|
436
|
-
|
|
464
|
+
// Only check if maskTextInputs is enabled, or if view is explicitly marked as sensitive
|
|
465
|
+
return (maskTextInputs || view.isSensitiveText()) && (hasText(view.text) || hasText(view.placeholder))
|
|
437
466
|
}
|
|
438
467
|
|
|
439
468
|
private func isSwiftUILayerSafe(_ layer: CALayer) -> Bool {
|
|
@@ -565,12 +594,17 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
565
594
|
)
|
|
566
595
|
|
|
567
596
|
|
|
568
|
-
// Use
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
597
|
+
// Use UIGraphicsImageRenderer (modern API) to render the resized image
|
|
598
|
+
let rendererFormat = UIGraphicsImageRendererFormat()
|
|
599
|
+
rendererFormat.scale = image.scale
|
|
600
|
+
rendererFormat.opaque = false
|
|
601
|
+
|
|
602
|
+
let renderer = UIGraphicsImageRenderer(size: newSize, format: rendererFormat)
|
|
603
|
+
let newImage = renderer.image { _ in
|
|
604
|
+
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return newImage
|
|
574
608
|
}
|
|
575
609
|
|
|
576
610
|
// MARK: - Gesture setup and handlers
|
|
@@ -754,6 +788,60 @@ class SessionRecorderNative: RCTEventEmitter, UIGestureRecognizerDelegate {
|
|
|
754
788
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
755
789
|
return true
|
|
756
790
|
}
|
|
791
|
+
|
|
792
|
+
// MARK: - Animation Transition Detection
|
|
793
|
+
/// Check if any view controller in the hierarchy is animating a transition
|
|
794
|
+
private func isAnimatingTransition(_ window: UIWindow) -> Bool {
|
|
795
|
+
guard let rootViewController = window.rootViewController else { return false }
|
|
796
|
+
return isAnimatingTransition(rootViewController)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private func isAnimatingTransition(_ viewController: UIViewController) -> Bool {
|
|
800
|
+
// Check if this view controller is animating
|
|
801
|
+
if viewController.transitionCoordinator?.isAnimated ?? false {
|
|
802
|
+
return true
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Check if presented view controller is animating
|
|
806
|
+
if let presented = viewController.presentedViewController, isAnimatingTransition(presented) {
|
|
807
|
+
return true
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Check if any of the child view controllers is animating
|
|
811
|
+
if viewController.children.first(where: { self.isAnimatingTransition($0) }) != nil {
|
|
812
|
+
return true
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return false
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// MARK: - Additional animation detection (layer + scroll)
|
|
819
|
+
/// Detects ongoing layer-based animations or actively scrolling scroll views.
|
|
820
|
+
private func windowHasActiveAnimations(_ window: UIWindow) -> Bool {
|
|
821
|
+
// Check for any Core Animation animations attached to the window's layer
|
|
822
|
+
if let keys = window.layer.animationKeys(), !keys.isEmpty {
|
|
823
|
+
return true
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check for actively scrolling UIScrollView instances
|
|
827
|
+
return hasAnimatingScrollView(in: window)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private func hasAnimatingScrollView(in view: UIView) -> Bool {
|
|
831
|
+
if let scrollView = view as? UIScrollView {
|
|
832
|
+
if scrollView.isDragging || scrollView.isDecelerating {
|
|
833
|
+
return true
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
for subview in view.subviews {
|
|
838
|
+
if hasAnimatingScrollView(in: subview) {
|
|
839
|
+
return true
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return false
|
|
844
|
+
}
|
|
757
845
|
}
|
|
758
846
|
|
|
759
847
|
private enum MaskingType {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["SessionRecorderSdk","MULTIPLAYER_BASE_API_URL","MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL","LogLevel","WidgetButtonPlacement","OTEL_MP_SAMPLE_TRACE_RATIO","DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE","mask","sensitiveFields","sensitiveHeaders","DEFAULT_MASKING_CONFIG","isContentMaskingEnabled","maskBody","maskHeaders","maskBodyFieldsList","maskHeadersList","headersToInclude","headersToExclude","maskImages","maskLabels","maskButtons","maskWebViews","maskTextInputs","maskSandboxedViews","DEFAULT_WIDGET_TEXT_CONFIG","initialTitleWithContinuous","initialTitleWithoutContinuous","initialDescriptionWithContinuous","initialDescriptionWithoutContinuous","continuousRecordingLabel","startRecordingButtonText","finalTitle","finalDescription","commentPlaceholder","saveButtonText","cancelButtonText","continuousOverlayTitle","continuousOverlayDescription","saveLastSnapshotButtonText","submitDialogTitle","submitDialogSubtitle","submitDialogCommentLabel","submitDialogCommentPlaceholder","submitDialogSubmitText","submitDialogCancelText","BASE_CONFIG","apiKey","version","application","environment","showContinuousRecording","widget","enabled","button","visible","placement","bottomRight","textOverrides","apiBaseUrl","exporterEndpoint","schemifyDocSpanPayload","ignoreUrls","propagateTraceHeaderCorsUrls","sampleTraceRatio","maxCapturingHttpPayloadSize","captureBody","captureHeaders","masking","recordScreen","recordGestures","recordNavigation","logger","level","INFO","useWebsocket"],"sourceRoot":"../../../src","sources":["config/defaults.ts"],"mappings":";;AAAA,SACEA,kBAAkB,EAClBC,wBAAwB,EACxBC,iDAAiD,QAC5C,0CAA0C;AACjD,SACEC,QAAQ,EACRC,qBAAqB,QAEhB,mBAAU;AACjB,SACEC,0BAA0B,EAC1BC,uCAAuC,QAClC,gBAAa;AAEpB,MAAM;EAAEC,IAAI;EAAEC,eAAe;EAAEC;AAAiB,CAAC,GAAGT,kBAAkB;AAEtE,OAAO,MAAMU,sBAAyD,GAAG;EACvEC,uBAAuB,EAAE,IAAI;EAC7BC,QAAQ,EAAEL,IAAI,CAACC,eAAe,CAAC;EAC/BK,WAAW,EAAEN,IAAI,CAACE,gBAAgB,CAAC;EACnCK,kBAAkB,EAAEN,eAAe;EACnCO,eAAe,EAAEN,gBAAgB;EACjCO,gBAAgB,EAAE,EAAE;EACpBC,gBAAgB,EAAE,EAAE;EACpB;EACAC,UAAU,EAAE,KAAK;EACjBC,UAAU,EAAE,KAAK;EACjBC,WAAW,EAAE,KAAK;EAClBC,YAAY,EAAE,KAAK;EACnBC,cAAc,EAAE,
|
|
1
|
+
{"version":3,"names":["SessionRecorderSdk","MULTIPLAYER_BASE_API_URL","MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL","LogLevel","WidgetButtonPlacement","OTEL_MP_SAMPLE_TRACE_RATIO","DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE","mask","sensitiveFields","sensitiveHeaders","DEFAULT_MASKING_CONFIG","isContentMaskingEnabled","maskBody","maskHeaders","maskBodyFieldsList","maskHeadersList","headersToInclude","headersToExclude","maskImages","maskLabels","maskButtons","maskWebViews","maskTextInputs","maskSandboxedViews","DEFAULT_WIDGET_TEXT_CONFIG","initialTitleWithContinuous","initialTitleWithoutContinuous","initialDescriptionWithContinuous","initialDescriptionWithoutContinuous","continuousRecordingLabel","startRecordingButtonText","finalTitle","finalDescription","commentPlaceholder","saveButtonText","cancelButtonText","continuousOverlayTitle","continuousOverlayDescription","saveLastSnapshotButtonText","submitDialogTitle","submitDialogSubtitle","submitDialogCommentLabel","submitDialogCommentPlaceholder","submitDialogSubmitText","submitDialogCancelText","BASE_CONFIG","apiKey","version","application","environment","showContinuousRecording","widget","enabled","button","visible","placement","bottomRight","textOverrides","apiBaseUrl","exporterEndpoint","schemifyDocSpanPayload","ignoreUrls","propagateTraceHeaderCorsUrls","sampleTraceRatio","maxCapturingHttpPayloadSize","captureBody","captureHeaders","masking","recordScreen","recordGestures","recordNavigation","logger","level","INFO","useWebsocket"],"sourceRoot":"../../../src","sources":["config/defaults.ts"],"mappings":";;AAAA,SACEA,kBAAkB,EAClBC,wBAAwB,EACxBC,iDAAiD,QAC5C,0CAA0C;AACjD,SACEC,QAAQ,EACRC,qBAAqB,QAEhB,mBAAU;AACjB,SACEC,0BAA0B,EAC1BC,uCAAuC,QAClC,gBAAa;AAEpB,MAAM;EAAEC,IAAI;EAAEC,eAAe;EAAEC;AAAiB,CAAC,GAAGT,kBAAkB;AAEtE,OAAO,MAAMU,sBAAyD,GAAG;EACvEC,uBAAuB,EAAE,IAAI;EAC7BC,QAAQ,EAAEL,IAAI,CAACC,eAAe,CAAC;EAC/BK,WAAW,EAAEN,IAAI,CAACE,gBAAgB,CAAC;EACnCK,kBAAkB,EAAEN,eAAe;EACnCO,eAAe,EAAEN,gBAAgB;EACjCO,gBAAgB,EAAE,EAAE;EACpBC,gBAAgB,EAAE,EAAE;EACpB;EACAC,UAAU,EAAE,KAAK;EACjBC,UAAU,EAAE,KAAK;EACjBC,WAAW,EAAE,KAAK;EAClBC,YAAY,EAAE,KAAK;EACnBC,cAAc,EAAE,KAAK;EACrBC,kBAAkB,EAAE;AACtB,CAAC;AAED,OAAO,MAAMC,0BAA6E,GAC1F;EACEC,0BAA0B,EAAE,uBAAuB;EACnDC,6BAA6B,EAAE,uBAAuB;EACtDC,gCAAgC,EAC9B,kEAAkE;EACpEC,mCAAmC,EACjC,kEAAkE;EACpEC,wBAAwB,EAAE,sBAAsB;EAChDC,wBAAwB,EAAE,iBAAiB;EAC3CC,UAAU,EAAE,iBAAiB;EAC7BC,gBAAgB,EACd,0FAA0F;EAC5FC,kBAAkB,EAAE,kBAAkB;EACtCC,cAAc,EAAE,kBAAkB;EAClCC,gBAAgB,EAAE,kBAAkB;EACpCC,sBAAsB,EAAE,mCAAmC;EAC3DC,4BAA4B,EAC1B,mOAAmO;EACrOC,0BAA0B,EAAE,gBAAgB;EAC5CC,iBAAiB,EAAE,gBAAgB;EACnCC,oBAAoB,EAClB,iJAAiJ;EACnJC,wBAAwB,EAAE,+CAA+C;EACzEC,8BAA8B,EAAE,kBAAkB;EAClDC,sBAAsB,EAAE,MAAM;EAC9BC,sBAAsB,EAAE;AAC1B,CAAC;AAED,OAAO,MAAMC,WAAmC,GAAG;EACjDC,MAAM,EAAE,EAAE;EAEVC,OAAO,EAAE,EAAE;EACXC,WAAW,EAAE,EAAE;EACfC,WAAW,EAAE,EAAE;EAEfC,uBAAuB,EAAE,IAAI;EAE7BC,MAAM,EAAE;IACNC,OAAO,EAAE,IAAI;IACbC,MAAM,EAAE;MACNC,OAAO,EAAE,IAAI;MACbC,SAAS,EAAEnD,qBAAqB,CAACoD;IACnC,CAAC;IACDC,aAAa,EAAEjC;EACjB,CAAC;EAEDkC,UAAU,EAAEzD,wBAAwB;EACpC0D,gBAAgB,EAAEzD,iDAAiD;EAEnE0D,sBAAsB,EAAE,IAAI;EAE5BC,UAAU,EAAE,EAAE;EACdC,4BAA4B,EAAE,EAAE;EAEhCC,gBAAgB,EAAE1D,0BAA0B;EAC5C2D,2BAA2B,EAAE1D,uCAAuC;EAEpE2D,WAAW,EAAE,IAAI;EACjBC,cAAc,EAAE,IAAI;EACpBC,OAAO,EAAEzD,sBAAsB;EAE/B0D,YAAY,EAAE,IAAI;EAClBC,cAAc,EAAE,IAAI;EACpBC,gBAAgB,EAAE,IAAI;EAEtBC,MAAM,EAAE;IACNnB,OAAO,EAAE,KAAK;IACdoB,KAAK,EAAErE,QAAQ,CAACsE;EAClB,CAAC;EAEDC,YAAY,EAAE;AAChB,CAAC","ignoreList":[]}
|
|
@@ -95,28 +95,17 @@ if (typeof fetch !== 'undefined' && typeof global !== 'undefined') {
|
|
|
95
95
|
|
|
96
96
|
// Capture request data
|
|
97
97
|
const inputIsRequest = typeof Request !== 'undefined' && input instanceof Request;
|
|
98
|
-
const safeToConstructRequest = !inputIsRequest || !input.bodyUsed;
|
|
99
|
-
|
|
100
|
-
// Only construct a new Request when it's safe (i.e., body not already used)
|
|
101
|
-
let requestForMetadata = null;
|
|
102
|
-
if (safeToConstructRequest) {
|
|
103
|
-
try {
|
|
104
|
-
requestForMetadata = new Request(input, init);
|
|
105
|
-
} catch {
|
|
106
|
-
requestForMetadata = null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
98
|
if (configs.recordRequestHeaders) {
|
|
110
|
-
if (
|
|
111
|
-
networkRequest.requestHeaders = _headersToObject(requestForMetadata.headers);
|
|
112
|
-
} else if (inputIsRequest) {
|
|
99
|
+
if (inputIsRequest) {
|
|
113
100
|
networkRequest.requestHeaders = _headersToObject(input.headers);
|
|
114
101
|
} else {
|
|
115
102
|
networkRequest.requestHeaders = _headersInitToObject(init?.headers);
|
|
116
103
|
}
|
|
117
104
|
}
|
|
118
105
|
if (configs.shouldRecordBody) {
|
|
119
|
-
|
|
106
|
+
// Only attempt to read the body from init (safe); avoid constructing/cloning Requests
|
|
107
|
+
// If the caller passed a Request as input, we do not attempt to read its body here
|
|
108
|
+
const candidateBody = init?.body;
|
|
120
109
|
if (!isNullish(candidateBody)) {
|
|
121
110
|
const requestBody = _tryReadFetchBody({
|
|
122
111
|
body: candidateBody
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["isFormData","isNullish","isObject","isString","formDataToQuery","configs","_tryReadFetchBody","body","JSON","stringify","Object","prototype","toString","call","_tryReadResponseBody","response","clonedResponse","clone","contentType","headers","get","includes","json","text","arrayBuffer","byteLength","error","Error","message","_headersToObject","result","forEach","value","key","_headersInitToObject","headersInit","Headers","Array","isArray","String","toLowerCase","entries","fetch","global","originalFetch","input","init","networkRequest","inputIsRequest","Request","
|
|
1
|
+
{"version":3,"names":["isFormData","isNullish","isObject","isString","formDataToQuery","configs","_tryReadFetchBody","body","JSON","stringify","Object","prototype","toString","call","_tryReadResponseBody","response","clonedResponse","clone","contentType","headers","get","includes","json","text","arrayBuffer","byteLength","error","Error","message","_headersToObject","result","forEach","value","key","_headersInitToObject","headersInit","Headers","Array","isArray","String","toLowerCase","entries","fetch","global","originalFetch","input","init","networkRequest","inputIsRequest","Request","recordRequestHeaders","requestHeaders","shouldRecordBody","candidateBody","requestBody","length","Blob","size","maxCapturingHttpPayloadSize","recordResponseHeaders","responseHeaders","responseBody","setPrototypeOf","defineProperty","console","warn","info"],"sourceRoot":"../../../src","sources":["patch/fetch.ts"],"mappings":"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","ignoreList":[]}
|