Skip to content

Commit

Permalink
Merge pull request #2182 from DataDog/hotfix/2.22.1
Browse files Browse the repository at this point in the history
hotfix/2.22.1

Co-authored-by: mariedm <[email protected]>
  • Loading branch information
dd-mergequeue[bot] and mariedm authored Jan 30, 2025
2 parents e49a8df + d0175e0 commit 0e33fbf
Show file tree
Hide file tree
Showing 31 changed files with 244 additions and 124 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Unreleased
# 2.22.1 / 30-01-2025

- [FIX] Fix memory leak in Session Replay where privacy overrides retained UIViews. See [#2182][]

# 2.22.0 / 02-01-2025

- [IMPROVEMENT] Add Datadog Configuration `backgroundTasksEnabled` ObjC API. See [#2148][]
- [FIX] Prevent Session Replay to create two full snapshots in a row. See [#2154][]
Expand Down Expand Up @@ -811,6 +815,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#2126]: https://github.com/DataDog/dd-sdk-ios/pull/2126
[#2148]: https://github.com/DataDog/dd-sdk-ios/pull/2148
[#2154]: https://github.com/DataDog/dd-sdk-ios/pull/2154
[#2182]: https://github.com/DataDog/dd-sdk-ios/pull/2182
[@00fa9a]: https://github.com/00FA9A
[@britton-earnin]: https://github.com/Britton-Earnin
[@hengyu]: https://github.com/Hengyu
Expand Down
2 changes: 1 addition & 1 deletion DatadogAlamofireExtension.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogAlamofireExtension"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire."
s.description = <<-DESC
The DatadogAlamofireExtension pod is deprecated and will no longer be maintained.
Expand Down
2 changes: 1 addition & 1 deletion DatadogCore.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogCore"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Official Datadog Swift SDK for iOS."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogCore/Sources/Versioning.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// GENERATED FILE: Do not edit directly

internal let __sdkVersion = "2.22.0"
internal let __sdkVersion = "2.22.1"
2 changes: 1 addition & 1 deletion DatadogCrashReporting.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogCrashReporting"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Official Datadog Crash Reporting SDK for iOS."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogInternal.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogInternal"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Datadog Internal Package. This module is not for public use."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogLogs.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogLogs"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Datadog Logs Module."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogObjc.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogObjc"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Official Datadog Objective-C SDK for iOS."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogRUM.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogRUM"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Datadog Real User Monitoring Module."

s.homepage = "https://www.datadoghq.com"
Expand Down
2 changes: 1 addition & 1 deletion DatadogSessionReplay.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "DatadogSessionReplay"
s.version = "2.22.0"
s.version = "2.22.1"
s.summary = "Official Datadog Session Replay SDK for iOS."

s.homepage = "https://www.datadoghq.com"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ internal class SnapshotTestCase: XCTestCase {
return viewController
}

// swiftlint:disable function_default_parameter_at_end
/// Helper method for most snapshot tests
func takeSnapshotFor( // swiftlint:disable:this function_default_parameter_at_end
func takeSnapshotFor(
_ fixture: Fixture,
with textAndInputPrivacyLevels: [TextAndInputPrivacyLevel] = [defaultTextAndInputPrivacyLevel],
imagePrivacyLevel: ImagePrivacyLevel = defaultImagePrivacyLevel,
Expand All @@ -73,6 +74,7 @@ internal class SnapshotTestCase: XCTestCase {
)
}
}
// swiftlint:enable function_default_parameter_at_end

/// Helper method for date and time picker snapshot tests
func takeSnapshotForPicker(
Expand Down Expand Up @@ -100,8 +102,9 @@ internal class SnapshotTestCase: XCTestCase {
}
}

// swiftlint:disable function_default_parameter_at_end
/// Helper method for snapshot tests showing PopupsViewController
func takeSnapshotForPopup( // swiftlint:disable:this function_default_parameter_at_end
func takeSnapshotForPopup(
fixture: Fixture,
showPopup: (PopupsViewController) -> Void,
waitTime: TimeInterval,
Expand All @@ -124,6 +127,7 @@ internal class SnapshotTestCase: XCTestCase {
)
}
}
// swiftlint:enable function_default_parameter_at_end

/// Captures side-by-side snapshot of the app UI and recorded wireframes.
func takeSnapshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle

var view: UIView? = initialView
while view != nil {
if let touchPrivacy = view?.dd.sessionReplayPrivacyOverrides.touchPrivacy {
if let touchPrivacy = view?.dd._privacyOverrides?.touchPrivacy {
return touchPrivacy
}
view = view?.superview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal class UIViewRecorder: NodeRecorder {
return semantics
}

if attributes.overrides.hide == true {
if attributes.hide == true {
let builder = UIViewWireframesBuilder(
wireframeID: context.ids.nodeID(view: view, nodeRecorder: self),
attributes: attributes
Expand Down Expand Up @@ -75,7 +75,7 @@ internal struct UIViewWireframesBuilder: NodeWireframesBuilder {
}

func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] {
if attributes.overrides.hide == true {
if attributes.hide == true {
return [
builder.createPlaceholderWireframe(
id: wireframeID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal struct ViewTreeRecorder {
/// Creates `Nodes` for given view and its subtree hierarchy.
func record(_ anyView: UIView, in context: ViewTreeRecordingContext) -> [Node] {
var nodes: [Node] = []
recordRecursively(nodes: &nodes, view: anyView, context: context, overrides: anyView.dd.sessionReplayPrivacyOverrides)
recordRecursively(nodes: &nodes, view: anyView, context: context, overrides: anyView.dd._privacyOverrides)
return nodes
}

Expand All @@ -27,7 +27,7 @@ internal struct ViewTreeRecorder {
nodes: inout [Node],
view: UIView,
context: ViewTreeRecordingContext,
overrides: PrivacyOverrides
overrides: PrivacyOverrides?
) {
var context = context
if let viewController = view.next as? UIViewController {
Expand Down Expand Up @@ -56,7 +56,7 @@ internal struct ViewTreeRecorder {
switch semantics.subtreeStrategy {
case .record:
for subview in view.subviews {
let subviewOverrides = SessionReplayPrivacyOverrides.merge(subview.dd.sessionReplayPrivacyOverrides, with: overrides)
let subviewOverrides = SessionReplayPrivacyOverrides.merge(subview.dd._privacyOverrides, with: overrides)
recordRecursively(nodes: &nodes, view: subview, context: context, overrides: subviewOverrides)
}
case .ignore:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,12 @@ public struct SessionReplayViewAttributes: Equatable {
/// Original view's `.intrinsicContentSize`.
var intrinsicContentSize: CGSize

/// If the view has privacy overrides, which take precedence over global masking privacy levels.
var overrides: PrivacyOverrides
/// Values copied from privacy overrides, if the view has privacy overrides,
/// which take precedence over global masking privacy levels.
var textAndInputPrivacy: TextAndInputPrivacyLevel?
var imagePrivacy: ImagePrivacyLevel?
var touchPrivacy: TouchPrivacyLevel?
var hide: Bool?
}

// This alias enables us to have a more unique name exposed through public-internal access level
Expand All @@ -114,7 +118,7 @@ extension ViewAttributes {
/// - view: The view instance.
/// - frame: The view frame in root view's coordinate space.
/// - clip: The clipping frame in root view's coordinate space.
init(view: UIView, frame: CGRect, clip: CGRect, overrides: SessionReplayPrivacyOverrides) {
init(view: UIView, frame: CGRect, clip: CGRect, overrides: SessionReplayPrivacyOverrides?) {
self.frame = frame
self.clip = clip
self.backgroundColor = view.backgroundColor?.cgColor.safeCast
Expand All @@ -124,7 +128,10 @@ extension ViewAttributes {
self.alpha = view.alpha
self.isHidden = view.isHidden
self.intrinsicContentSize = view.intrinsicContentSize
self.overrides = overrides
self.textAndInputPrivacy = overrides?.textAndInputPrivacy
self.imagePrivacy = overrides?.imagePrivacy
self.touchPrivacy = overrides?.touchPrivacy
self.hide = overrides?.hide
}

/// If the view is technically visible (different than `!isHidden` because it also considers `alpha` and `frame != .zero`).
Expand Down Expand Up @@ -165,13 +172,13 @@ extension ViewAttributes {
/// Resolves the effective privacy level for text and input elements by considering the view's local override.
/// Falls back to the global privacy setting in the absence of local overrides.
func resolveTextAndInputPrivacyLevel(in context: ViewTreeRecordingContext) -> TextAndInputPrivacyLevel {
return self.overrides.textAndInputPrivacy ?? context.recorder.textAndInputPrivacy
return self.textAndInputPrivacy ?? context.recorder.textAndInputPrivacy
}

/// Resolves the effective privacy level for image elements by considering the view's local override.
/// Falls back to the global privacy setting in the absence of local overrides.
func resolveImagePrivacyLevel(in context: ViewTreeRecordingContext) -> ImagePrivacyLevel {
return self.overrides.imagePrivacy ?? context.recorder.imagePrivacy
return self.imagePrivacy ?? context.recorder.imagePrivacy
}
}

Expand Down
103 changes: 43 additions & 60 deletions DatadogSessionReplay/Sources/SessionReplayPrivacyOverrides.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,105 +8,88 @@
import UIKit
import DatadogInternal

// MARK: - Associated Keys

internal var associatedOverridesKey: UInt8 = 3

// MARK: - DatadogExtension for UIView

/// Extension to provide access to `SessionReplayPrivacyOverrides` for any `UIView`.
extension DatadogExtension where ExtendedType: UIView {
/// Provides access to Session Replay override settings for the view.
/// Usage: `myView.dd.sessionReplayPrivacyOverrides.textAndInputPrivacy = .maskNone`.
public var sessionReplayPrivacyOverrides: SessionReplayPrivacyOverrides {
return SessionReplayPrivacyOverrides(self.type)
}
}
if let overrides = _privacyOverrides {
return overrides
}

// MARK: - Associated Keys
let overrides = SessionReplayPrivacyOverrides()
objc_setAssociatedObject(type, &associatedOverridesKey, overrides, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return overrides
}

private var associatedTextAndInputPrivacyKey: UInt8 = 3
private var associatedImagePrivacyKey: UInt8 = 4
private var associatedTouchPrivacyKey: UInt8 = 5
private var associatedHiddenPrivacyKey: UInt8 = 6
/// Internal accessor
internal var _privacyOverrides: SessionReplayPrivacyOverrides? {
objc_getAssociatedObject(type, &associatedOverridesKey) as? SessionReplayPrivacyOverrides
}
}

// MARK: - SessionReplayPrivacyOverrides

/// `UIView` extension to manage the Session Replay privacy override settings.
public final class SessionReplayPrivacyOverrides {
internal let view: UIView

public init(_ view: UIView) {
self.view = view
}

/// Text and input privacy override (e.g., mask or unmask specific text fields, labels, etc.).
public var textAndInputPrivacy: TextAndInputPrivacyLevel? {
get {
return objc_getAssociatedObject(view, &associatedTextAndInputPrivacyKey) as? TextAndInputPrivacyLevel
}
set {
objc_setAssociatedObject(view, &associatedTextAndInputPrivacyKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}

public var textAndInputPrivacy: TextAndInputPrivacyLevel?
/// Image privacy override (e.g., mask or unmask specific images).
public var imagePrivacy: ImagePrivacyLevel? {
get {
return objc_getAssociatedObject(view, &associatedImagePrivacyKey) as? ImagePrivacyLevel
}
set {
objc_setAssociatedObject(view, &associatedImagePrivacyKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}

public var imagePrivacy: ImagePrivacyLevel?
/// Touch privacy override (e.g., hide or show touch interactions on specific views).
public var touchPrivacy: TouchPrivacyLevel? {
get {
return objc_getAssociatedObject(view, &associatedTouchPrivacyKey) as? TouchPrivacyLevel
}
set {
objc_setAssociatedObject(view, &associatedTouchPrivacyKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}

public var touchPrivacy: TouchPrivacyLevel?
/// Hidden privacy override (e.g., mark a view as hidden, rendering it as an opaque wireframe in replays).
public var hide: Bool? {
get {
return objc_getAssociatedObject(view, &associatedHiddenPrivacyKey) as? Bool
}
set {
objc_setAssociatedObject(view, &associatedHiddenPrivacyKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
public var hide: Bool?

/// Creates a new instance of privacy overrides.
internal init() {
// Initialize with all properties as nil
}
}

// MARK: - Equatable

extension PrivacyOverrides: Equatable {
public static func == (lhs: SessionReplayPrivacyOverrides, rhs: SessionReplayPrivacyOverrides) -> Bool {
return lhs.view === rhs.view
&& lhs.textAndInputPrivacy == rhs.textAndInputPrivacy
return lhs.textAndInputPrivacy == rhs.textAndInputPrivacy
&& lhs.imagePrivacy == rhs.imagePrivacy
&& lhs.touchPrivacy == rhs.touchPrivacy
&& lhs.hide == rhs.hide
}
}

// MARK: - Merge

extension PrivacyOverrides {
/// Merges child and parent overrides, giving precedence to the child’s overrides, if set.
/// If the child has no overrides set, it inherits its parent’s overrides.
internal static func merge(_ child: PrivacyOverrides, with parent: PrivacyOverrides) -> PrivacyOverrides {
let merged = child
internal static func merge(_ child: PrivacyOverrides?, with parent: PrivacyOverrides?) -> PrivacyOverrides? {
guard let child = child else {
return parent
}
guard let parent = parent else {
return child
}

// Apply parent overrides where child has none set
child.textAndInputPrivacy = child.textAndInputPrivacy ?? parent.textAndInputPrivacy
child.imagePrivacy = child.imagePrivacy ?? parent.imagePrivacy
child.touchPrivacy = child.touchPrivacy ?? parent.touchPrivacy

// Apply child overrides if present
merged.textAndInputPrivacy = merged.textAndInputPrivacy ?? parent.textAndInputPrivacy
merged.imagePrivacy = merged.imagePrivacy ?? parent.imagePrivacy
merged.touchPrivacy = merged.touchPrivacy ?? parent.touchPrivacy
/// `hide` is a boolean, so we explicitly check if either the parent or the child has it set to `true`.
/// `false` and `nil` behave the same way, it deactivates the `hide` override.
/// `false` and `nil` behave the same way, it deactivates the `hide` override.
/// In practice, this check should not hit, as parent views with `hide = true` should ignore their children.
if merged.hide == true || parent.hide == true {
merged.hide = true
if child.hide == true || parent.hide == true {
child.hide = true
}

return merged
return child
}
}

Expand Down
Loading

0 comments on commit 0e33fbf

Please sign in to comment.