Skip to content

Commit

Permalink
Merge 2d12ef7 into ed470cf
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Nov 4, 2024
2 parents ed470cf + 2d12ef7 commit eaf2690
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Log a warning when dropping envelopes due to rate-limiting (#4463)
- Expose `SentrySessionReplayIntegration-Hybrid.h` as `private` (#4486)
- Add `maskedViewClasses` and `unmaskedViewClasses` to SentryReplayOptions init via dict (#4492)
- Add `quality` to SentryReplayOptions init via dict (#4495)

## 8.39.0

Expand Down
63 changes: 41 additions & 22 deletions Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,35 @@ import Foundation

@objcMembers
public class SentryReplayOptions: NSObject, SentryRedactOptions {

/**
* Enum to define the quality of the session replay.
*/
@objc
public enum SentryReplayQuality: Int {
static let names = [ "low", "medium", "high"]

/**
* Video Scale: 80%
* Bit Rate: 20.000
*/
case low
case low = 0

/**
* Video Scale: 100%
* Bit Rate: 40.000
*/
case medium
case medium = 1

/**
* Video Scale: 100%
* Bit Rate: 60.000
*/
case high
case high = 2
}


private static let defaultQuality: SentryReplayQuality = .medium

/**
* Indicates the percentage in which the replay for the session will be created.
* - Specifying @c 0 means never, @c 1.0 means always.
Expand All @@ -44,60 +48,60 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* - note: The default is 0.
*/
public var onErrorSampleRate: Float

/**
* Indicates whether session replay should redact all text in the app
* by drawing a black rectangle over it.
*
* - note: The default is true
*/
public var maskAllText = true

/**
* Indicates whether session replay should redact all non-bundled image
* in the app by drawing a black rectangle over it.
*
* - note: The default is true
*/
public var maskAllImages = true

/**
* Indicates the quality of the replay.
* The higher the quality, the higher the CPU and bandwidth usage.
*/
public var quality = SentryReplayQuality.medium

/**
* A list of custom UIView subclasses that need
* A list of custom UIView subclasses that need
* to be masked during session replay.
* By default Sentry already mask text and image elements from UIKit
* Every child of a view that is redacted will also be redacted.
*/
public var maskedViewClasses = [AnyClass]()

/**
* A list of custom UIView subclasses to be ignored
* during masking step of the session replay.
* The views of given classes will not be redacted but their children may be.
* This property has precedence over `redactViewTypes`.
*/
public var unmaskedViewClasses = [AnyClass]()

/**
* Defines the quality of the session replay.
* Higher bit rates better quality, but also bigger files to transfer.
*/
var replayBitRate: Int {
quality.rawValue * 20_000 + 20_000
}

/**
* The scale related to the window size at which the replay will be created
*/
var sizeScale: Float {
quality == .low ? 0.8 : 1.0
}

/**
* Number of frames per second of the replay.
* The more the havier the process is.
Expand All @@ -108,30 +112,30 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
if frameRate < 1 { frameRate = 1 }
}
}

/**
* The maximum duration of replays for error events.
*/
let errorReplayDuration = TimeInterval(30)

/**
* The maximum duration of the segment of a session replay.
*/
let sessionSegmentDuration = TimeInterval(5)

/**
* The maximum duration of a replay session.
*/
let maximumDuration = TimeInterval(3_600)

/**
* Inittialize session replay options disabled
*/
public override init() {
self.sessionSampleRate = 0
self.onErrorSampleRate = 0
}

/**
* Initialize session replay options
* - parameters:
Expand All @@ -145,7 +149,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
self.maskAllText = maskAllText
self.maskAllImages = maskAllImages
}

convenience init(dictionary: [String: Any]) {
let sessionSampleRate = (dictionary["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0
let onErrorSampleRate = (dictionary["errorSampleRate"] as? NSNumber)?.floatValue ?? 0
Expand All @@ -158,5 +162,20 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
self.unmaskedViewClasses = ((dictionary["unmaskedViewClasses"] as? NSArray) ?? []).compactMap({ element in
NSClassFromString((element as? String) ?? "")
})
if let quality = (dictionary["quality"] as? String) {
self.quality = SentryReplayQuality.fromName(quality)
}

}
}

extension SentryReplayOptions.SentryReplayQuality: CustomStringConvertible {
public var description: String {
return SentryReplayOptions.SentryReplayQuality.names[Int(self.rawValue)]
}

static func fromName(_ name: String) -> SentryReplayOptions.SentryReplayQuality {
guard let index = SentryReplayOptions.SentryReplayQuality.names.firstIndex(of: name) else { return SentryReplayOptions.defaultQuality }
return SentryReplayOptions.SentryReplayQuality(rawValue: Int(index)) ?? SentryReplayOptions.defaultQuality
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,38 @@ import Foundation
import XCTest

class SentryReplayOptionsTests: XCTestCase {

func testQualityLow() {
let options = SentryReplayOptions()
options.quality = .low

XCTAssertEqual(options.replayBitRate, 20_000)
XCTAssertEqual(options.sizeScale, 0.8)
}

func testQualityMedium() {
let options = SentryReplayOptions()
options.quality = .medium

XCTAssertEqual(options.replayBitRate, 40_000)
XCTAssertEqual(options.sizeScale, 1.0)
}

func testQualityHigh() {
let options = SentryReplayOptions()
options.quality = .high

XCTAssertEqual(options.replayBitRate, 60_000)
XCTAssertEqual(options.sizeScale, 1.0)
}

func testQualityFromName() {
XCTAssertEqual(SentryReplayOptions.SentryReplayQuality.fromName("low"), .low)
XCTAssertEqual(SentryReplayOptions.SentryReplayQuality.fromName("medium"), .medium)
XCTAssertEqual(SentryReplayOptions.SentryReplayQuality.fromName("high"), .high)
XCTAssertEqual(SentryReplayOptions.SentryReplayQuality.fromName("invalid_value"), .medium)
}

func testInitFromDictOnErrorSampleRateAsDouble() {
let options = SentryReplayOptions(dictionary: [
"errorSampleRate": 0.44
Expand Down Expand Up @@ -160,13 +167,43 @@ class SentryReplayOptionsTests: XCTestCase {
XCTAssertTrue(options.maskAllImages)
}

func testInitFromDictQualityWithString() {
let options = SentryReplayOptions(dictionary: [
"quality": "low"
])
XCTAssertEqual(options.quality, .low)

let options2 = SentryReplayOptions(dictionary: [
"quality": "medium"
])
XCTAssertEqual(options2.quality, .medium)

let options3 = SentryReplayOptions(dictionary: [
"quality": "high"
])
XCTAssertEqual(options3.quality, .high)
}

func testInitFromDictQualityWithInvalidValue() {
let options = SentryReplayOptions(dictionary: [
"quality": "invalid_value"
])
XCTAssertEqual(options.quality, .medium)

let options2 = SentryReplayOptions(dictionary: [
"quality": [1]
])
XCTAssertEqual(options2.quality, .medium)
}

func testInitFromDictWithMultipleOptions() {
let options = SentryReplayOptions(dictionary: [
"sessionSampleRate": 0.5,
"errorSampleRate": 0.8,
"maskAllText": false,
"maskedViewClasses": ["NSString", "not.a.class", 123] as [Any],
"unmaskedViewClasses": ["NSNumber", "invalid", true] as [Any]
"unmaskedViewClasses": ["NSNumber", "invalid", true] as [Any],
"quality": "low"
])

XCTAssertEqual(options.sessionSampleRate, 0.5)
Expand All @@ -177,5 +214,7 @@ class SentryReplayOptionsTests: XCTestCase {
XCTAssertEqual(ObjectIdentifier(options.maskedViewClasses.first!), ObjectIdentifier(NSString.self))
XCTAssertEqual(options.unmaskedViewClasses.count, 1)
XCTAssertEqual(ObjectIdentifier(options.unmaskedViewClasses.first!), ObjectIdentifier(NSNumber.self))
XCTAssertEqual(options.quality, .low)
}

}

0 comments on commit eaf2690

Please sign in to comment.