From 952023ca430b0175146847b2c4676705e21c9af6 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Wed, 27 Nov 2024 10:58:19 +0100 Subject: [PATCH 1/3] RUM-7176 Record SwiftUI raster images --- Datadog/Datadog.xcodeproj/project.pbxproj | 24 ++++ .../Utilities/CGImage+SessionReplay.swift | 23 ++++ .../SwfitUIImage+SessionReplay.swift | 26 ++++ .../Utilities/UIImage+SessionReplay.swift | 4 +- .../NodeRecorders/SwiftUI/Color.swift | 2 +- .../SwiftUI/DisplayList+Reflection.swift | 5 +- .../NodeRecorders/SwiftUI/DisplayList.swift | 3 +- .../SwiftUI/Image+Reflection.swift | 32 +++++ .../NodeRecorders/SwiftUI/Image.swift | 37 ++++++ .../SwiftUI/SwiftUIWireframesBuilder.swift | 37 ++++++ .../NodeRecorders/SwiftUI/Text.swift | 2 +- .../Tests/ImageReflectionTests.swift | 83 +++++++++++++ .../Tests/Mocks/ReflectionMocks.swift | 114 ++++++++++++++++++ 13 files changed, 385 insertions(+), 7 deletions(-) create mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image+Reflection.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift create mode 100644 DatadogSessionReplay/Tests/ImageReflectionTests.swift create mode 100644 DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index f77708fb54..fdceee7e4a 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -684,6 +684,12 @@ 962C41A72CA431370050B747 /* SessionReplayPrivacyOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */; }; 962C41A82CA431AA0050B747 /* DDSessionReplayOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E863752C9C7E800023BF78 /* DDSessionReplayOverridesTests.swift */; }; 962C41A92CB00FD60050B747 /* DDSessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */; }; + 962D72BC2CF6436700F86EF0 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BA2CF6436600F86EF0 /* Image.swift */; }; + 962D72BD2CF6436700F86EF0 /* Image+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */; }; + 962D72BF2CF7538800F86EF0 /* CGImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */; }; + 962D72C12CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */; }; + 962D72C52CF7806300F86EF0 /* ImageReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C42CF7806300F86EF0 /* ImageReflectionTests.swift */; }; + 962D72C72CF7815300F86EF0 /* ReflectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */; }; 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */; }; 969B3B232C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */; }; 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */; }; @@ -2755,6 +2761,12 @@ 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventFileOutputTests.swift; sourceTree = ""; }; 61FF416125EE5FF400CE35EC /* CrashLogReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashLogReceiverTests.swift; sourceTree = ""; }; 61FF9A4425AC5DEA001058CC /* ViewIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewIdentifier.swift; sourceTree = ""; }; + 962D72BA2CF6436600F86EF0 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+Reflection.swift"; sourceTree = ""; }; + 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+SessionReplay.swift"; sourceTree = ""; }; + 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwfitUIImage+SessionReplay.swift"; sourceTree = ""; }; + 962D72C42CF7806300F86EF0 /* ImageReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageReflectionTests.swift; sourceTree = ""; }; + 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflectionMocks.swift; sourceTree = ""; }; 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayPrivacyOverrides.swift; sourceTree = ""; }; 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityIndicatorRecorder.swift; sourceTree = ""; }; 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityIndicatorRecorderTests.swift; sourceTree = ""; }; @@ -3635,6 +3647,7 @@ 61054F592A6EE1BA00AAA894 /* Recorder */, 61054F3E2A6EE1B900AAA894 /* Utilities */, 61054F492A6EE1BA00AAA894 /* Writer */, + 962D72C42CF7806300F86EF0 /* ImageReflectionTests.swift */, ); name = DatadogSessionReplayTests; path = ../DatadogSessionReplay/Tests; @@ -3677,6 +3690,8 @@ isa = PBXGroup; children = ( 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */, + 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */, + 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */, D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */, 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, @@ -4055,6 +4070,7 @@ A74A72862B10CE4100771FEB /* ResourceMocks.swift */, A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */, + 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */, ); path = Mocks; sourceTree = ""; @@ -6465,6 +6481,8 @@ D2AD1CBE2CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift */, D2AD1CBF2CE4AE6600106C74 /* Text.swift */, D2AD1CC02CE4AE6600106C74 /* Text+Reflection.swift */, + 962D72BA2CF6436600F86EF0 /* Image.swift */, + 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */, ); path = SwiftUI; sourceTree = ""; @@ -8453,6 +8471,7 @@ 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */, 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */, 61054E982A6EE10A00AAA894 /* RecordsBuilder.swift in Sources */, + 962D72C12CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift in Sources */, 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */, 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */, 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */, @@ -8460,6 +8479,7 @@ 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */, + 962D72BF2CF7538800F86EF0 /* CGImage+SessionReplay.swift in Sources */, 96F25A832CC7EA4400459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift in Sources */, 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */, 61054E832A6EE10A00AAA894 /* UISwitchRecorder.swift in Sources */, @@ -8472,8 +8492,10 @@ 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */, 61054E722A6EE10A00AAA894 /* TouchIdentifierGenerator.swift in Sources */, A7B932F52B1F694000AE6477 /* ResourcesProcessor.swift in Sources */, + 962D72BD2CF6436700F86EF0 /* Image+Reflection.swift in Sources */, 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */, + 962D72BC2CF6436700F86EF0 /* Image.swift in Sources */, 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */, 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */, D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */, @@ -8555,6 +8577,7 @@ 61054FB62A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift in Sources */, 61054F9F2A6EE1BA00AAA894 /* RecordsWriterTests.swift in Sources */, 61054FB82A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift in Sources */, + 962D72C52CF7806300F86EF0 /* ImageReflectionTests.swift in Sources */, 61054FA32A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift in Sources */, 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */, 61054FC52A6EE1BA00AAA894 /* UIKitMocks.swift in Sources */, @@ -8574,6 +8597,7 @@ 61054FD32A6EE1BA00AAA894 /* MultipartFormDataTests.swift in Sources */, 61054FB12A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift in Sources */, 61054F9C2A6EE1BA00AAA894 /* SwiftExtensionsTests.swift in Sources */, + 962D72C72CF7815300F86EF0 /* ReflectionMocks.swift in Sources */, 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */, 61054F9D2A6EE1BA00AAA894 /* MainThreadSchedulerTests.swift in Sources */, 61054FAA2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift in Sources */, diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift new file mode 100644 index 0000000000..09faa3f35e --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift @@ -0,0 +1,23 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) +import CoreGraphics + +/// Same heuristic as Android to determine if an image is likely bundled: +/// Icons and small assets usually have dimensions <= 100 points. +internal extension CGImage { + func isLikelyBundled(scale: CGFloat) -> Bool { + let renderedSize = self.renderedSize(scale: scale) + let maxDimension: CGFloat = 100 + return renderedSize.width <= maxDimension && renderedSize.height <= maxDimension + } + + fileprivate func renderedSize(scale: CGFloat) -> CGSize { + return CGSize(width: CGFloat(width) / scale, height: CGFloat(height) / scale) + } +} +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift new file mode 100644 index 0000000000..5e2758f333 --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift @@ -0,0 +1,26 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) +import SwiftUI + +/// Mapping SwiftUI orientation to UIImage orientation +@available(iOS 13.0, tvOS 13.0, *) +internal extension SwiftUI.Image.Orientation { + var uiImageOrientation: UIImage.Orientation { + switch self { + case .up: return .up + case .down: return .down + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + } + } +} +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/UIImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/UIImage+SessionReplay.swift index a57edd4aaa..e917af0e62 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/UIImage+SessionReplay.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/UIImage+SessionReplay.swift @@ -81,7 +81,7 @@ extension DatadogExtension where ExtendedType: UIImage { /// Compress the image to PNG. /// - /// Scale down the image an apply tint color if necessary. + /// Scale down the image and apply tint color if necessary. /// /// - Parameters: /// - maxSize: The maximum size of the image. @@ -97,7 +97,7 @@ extension DatadogExtension where ExtendedType: UIImage { /// Compress an image to PNG. /// - /// Scale down the image an apply tint color if necessary. + /// Scale down the image and apply tint color if necessary. /// /// - Parameters: /// - image: The image to compress. diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Color.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Color.swift index 5197773a65..6fe8696deb 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Color.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Color.swift @@ -4,7 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ - #if os(iOS) +#if os(iOS) import Foundation import SwiftUI diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList+Reflection.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList+Reflection.swift index 8570410b05..7ec16d4589 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList+Reflection.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList+Reflection.swift @@ -137,8 +137,9 @@ extension DisplayList.Content.Value: Reflection { case (.enum("platformView"), _): self = .platformView - case (.enum("image"), _): - self = .unknown + case (.enum("image"), let graphicsImage): + let resolvedImage = try GraphicsImage(reflecting: graphicsImage) + self = .image(resolvedImage) case (.enum("drawing"), _): self = .unknown diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList.swift index 26f2180734..8e0dc9f3f6 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/DisplayList.swift @@ -4,7 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ - #if os(iOS) +#if os(iOS) import Foundation import SwiftUI @@ -83,6 +83,7 @@ internal struct DisplayList { case text(StyledTextContentView, CGSize) case platformView case color(Color._Resolved) + case image(GraphicsImage) case unknown } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image+Reflection.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image+Reflection.swift new file mode 100644 index 0000000000..01da7bbbec --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image+Reflection.swift @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) + +import CoreGraphics +import SwiftUI + +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage: Reflection { + init(_ mirror: ReflectionMirror) throws { + scale = try mirror.descendant("scale") + orientation = try mirror.descendant("orientation") + contents = try mirror.descendant("contents") + } +} + +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage.Contents: Reflection { + init(_ mirror: ReflectionMirror) throws { + switch (mirror.displayStyle, mirror.descendant(0)) { + case let (.enum("cgImage"), cgImage as CGImage): + self = .cgImage(cgImage) + default: + self = .unknown + } + } +} +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift new file mode 100644 index 0000000000..9f1f0ff1bf --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift @@ -0,0 +1,37 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) +import CoreGraphics +import SwiftUI + +/// Represents a SwiftUI.GraphicsImage +@available(iOS 13.0, tvOS 13.0, *) +internal struct GraphicsImage { + let contents: Contents + let scale: CGFloat + let orientation: SwiftUI.Image.Orientation + + enum Contents { + case cgImage(CGImage) + case unknown + } +} + +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage.Contents: Equatable { + static func == (lhs: GraphicsImage.Contents, rhs: GraphicsImage.Contents) -> Bool { + switch (lhs, rhs) { + case let (.cgImage(lImage), .cgImage(rImage)): + return lImage === rImage + case (.unknown, .unknown): + return true + default: + return false + } + } +} +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift index aa20087ae7..75a86debb1 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift @@ -131,6 +131,43 @@ internal struct SwiftUIWireframesBuilder: NodeWireframesBuilder { cornerRadius: viewInfo?.cornerRadius, opacity: viewInfo?.alpha ) + case let .image(resolvedImage): + switch resolvedImage.contents { + case .cgImage(let cgImage): + // TODO: RUM-7462 - Apply global privacy setting + // TODO: RUM-7370 - Apply FGM overrides + if cgImage.isLikelyBundled(scale: resolvedImage.scale) { + let imageResource = UIImageResource( + image: UIImage( + cgImage: cgImage, + scale: resolvedImage.scale, + orientation: resolvedImage.orientation.uiImageOrientation + ), + tintColor: nil + ) + return context.builder.createImageWireframe( + id: Int64(content.seed.value), + resource: imageResource, + frame: context.convert(frame: item.frame), + clip: context.clip + ) + } else { + return context.builder.createPlaceholderWireframe( + id: Int64(content.seed.value), + frame: item.frame, + clip: context.clip, + label: "Content Image" + ) + } + case .unknown: + return context.builder.createPlaceholderWireframe( + id: Int64(content.seed.value), + frame: item.frame, + clip: context.clip, + label: "Unsupported image type" + ) + } + case .platformView: return nil // Should be recorder by UIKit recorder case .unknown: diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Text.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Text.swift index db9c794f1f..58088e6080 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Text.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Text.swift @@ -4,7 +4,7 @@ * Copyright 2019-Present Datadog, Inc. */ - #if os(iOS) +#if os(iOS) import Foundation import SwiftUI diff --git a/DatadogSessionReplay/Tests/ImageReflectionTests.swift b/DatadogSessionReplay/Tests/ImageReflectionTests.swift new file mode 100644 index 0000000000..2003ce6f96 --- /dev/null +++ b/DatadogSessionReplay/Tests/ImageReflectionTests.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) +import XCTest +import DatadogInternal +import CoreGraphics +import SwiftUI +@testable import DatadogSessionReplay + +@available(iOS 13.0, tvOS 13.0, *) +class ImageReflectionTests: XCTestCase { + func testGraphicsImageReflection() { + let contents: GraphicsImage.Contents = .mockAny() + let scale: CGFloat = [1, 2, 3].randomElement()! + let orientation: SwiftUI.Image.Orientation = .mockAny() + let graphicsImage = GraphicsImage( + contents: contents, + scale: scale, + orientation: orientation + ) + XCTAssertNotNil(graphicsImage) + XCTAssertEqual(graphicsImage.contents, contents) + XCTAssertEqual(graphicsImage.scale, scale) + XCTAssertEqual(graphicsImage.orientation, orientation) + } + + func testGraphicsImageContentsReflectionWithCGImage() { + let cgImage: CGImage = MockCGImage.mockWith(width: 20) + let contents: GraphicsImage.Contents = .cgImage(cgImage) + XCTAssertEqual(contents, .cgImage(cgImage)) + } + + func testGraphicsImageContentsReflectionWithUnknown() { + let contents: GraphicsImage.Contents = .unknown + XCTAssertEqual(contents, .unknown) + } + + func testEnumCaseForCGImageGraphicsImageContents() { + let cgImage: CGImage = MockCGImage.mockWith(width: 20) + let contents: GraphicsImage.Contents = .cgImage(cgImage) + + switch contents { + case .cgImage: + XCTAssertTrue(true) + default: + XCTFail("Expected .cgImage, got \(String(describing: contents))") + } + } + + func testReflectionParsingForGraphicsImage() { + let graphicsImage = GraphicsImage.mockAny() + let mirror = ReflectionMirror(reflecting: graphicsImage) + XCTAssertNotNil(mirror) + XCTAssertEqual(mirror.displayStyle, .struct) + } + + func testGraphicsImageContentsEquality() { + let cgImage1: CGImage = MockCGImage.mockWith(width: 20) + let cgImage2: CGImage = MockCGImage.mockWith(width: 20) + + // Check equality + let contents1 = GraphicsImage.Contents.cgImage(cgImage1) + let contents2 = GraphicsImage.Contents.cgImage(cgImage1) + XCTAssertEqual(contents1, contents2, "GraphicsImage.Contents should be equal if cgImage is the same.") + + // Check inequality + let contents3 = GraphicsImage.Contents.cgImage(cgImage2) + XCTAssertNotEqual(contents1, contents3, "GraphicsImage.Contents should not be equal for different cgImages.") + } + + func testCGImageBundlingHeuristic() { + let smallImage: CGImage = MockCGImage.mockWith(width: 100) + XCTAssertTrue(smallImage.isLikelyBundled(scale: 1.0)) + + let largeImage: CGImage = MockCGImage.mockWith(width: 150) + XCTAssertFalse(largeImage.isLikelyBundled(scale: 1.0)) + } +} +#endif diff --git a/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift b/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift new file mode 100644 index 0000000000..f1564f640f --- /dev/null +++ b/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift @@ -0,0 +1,114 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) +@testable import DatadogSessionReplay +import TestUtilities +import SwiftUI +import CoreGraphics + +private let bytesPerPixel = 4 + +private extension Int { + static func mockImageDimension() -> Int { + let min = 1, max = 10_000 + return Int.random(in: min...max) + } +} + +/// A mock implementation of `CGImage` for unit tests. +/// - Note: This mock creates a dummy RGBA image of configurable size. +/// - Warning: Ensure the width is within acceptable limits to avoid memory issues. +struct MockCGImage: RandomMockable { + let cgImage: CGImage + + init(cgImage: CGImage) { + self.cgImage = cgImage + } + + static func mockRandom() -> MockCGImage { + return MockCGImage( + cgImage: mockWith() + ) + } + + static func mockWith( + width: Int = .mockImageDimension() + ) -> CGImage { + precondition(width > 0, "Width must be greater than 0") + + let totalBytes = width * width * bytesPerPixel + + // Allocate memory for pixel data + guard let data = malloc(totalBytes) else { + fatalError("Failed to allocate memory for pixel data") + } + + // Free the allocated memory when the function exits + defer { free(data) } + + // Fill the allocated memory with dummy data + memset(data, 255, totalBytes) + + let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! + let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue + guard let context = CGContext( + data: data, + width: width, + height: width, + bitsPerComponent: 8, + bytesPerRow: width * bytesPerPixel, + space: colorSpace, + bitmapInfo: bitmapInfo + ) else { + fatalError("Failed to create CGContext for mock CGImage") + } + + return context.makeImage()! + } +} + +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage: AnyMockable, RandomMockable { + public static func mockAny() -> GraphicsImage { + return GraphicsImage( + contents: .cgImage(MockCGImage.mockRandom().cgImage), + scale: 1.0, + orientation: .up + ) + } + + public static func mockRandom() -> GraphicsImage { + return GraphicsImage( + contents: .cgImage(MockCGImage.mockRandom().cgImage), + scale: CGFloat.random(in: 0.5...3.0), + orientation: .allCases.randomElement()! + ) + } +} + +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage.Contents: AnyMockable, RandomMockable { + public static func mockAny() -> GraphicsImage.Contents { + return .cgImage(MockCGImage.mockRandom().cgImage) + } + + public static func mockRandom() -> GraphicsImage.Contents { + return [.cgImage(MockCGImage.mockRandom().cgImage), .unknown].randomElement()! + } +} + +@available(iOS 13.0, tvOS 13.0, *) +extension SwiftUI.Image.Orientation: AnyMockable, RandomMockable { + public static func mockAny() -> SwiftUI.Image.Orientation { + return SwiftUI.Image.Orientation.up + } + + public static func mockRandom() -> SwiftUI.Image.Orientation { + return SwiftUI.Image.Orientation.allCases.randomElement()! + } +} +#endif From f9df9407c8fe42039c9884714fe2e6ab47c0fa38 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Tue, 3 Dec 2024 10:41:00 +0100 Subject: [PATCH 2/3] RUM-7176 Address CR comments + improve testCGImageBundlingHeuristic --- Datadog/Datadog.xcodeproj/project.pbxproj | 4 -- .../Utilities/CGImage+SessionReplay.swift | 7 ++- .../SwfitUIImage+SessionReplay.swift | 26 -------- .../NodeRecorders/SwiftUI/Image.swift | 21 ++++--- .../SwiftUI/SwiftUIWireframesBuilder.swift | 2 +- .../Tests/ImageReflectionTests.swift | 27 +++++++-- .../Tests/Mocks/ReflectionMocks.swift | 60 +++++++++++-------- 7 files changed, 76 insertions(+), 71 deletions(-) delete mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index fdceee7e4a..a6bda14208 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -687,7 +687,6 @@ 962D72BC2CF6436700F86EF0 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BA2CF6436600F86EF0 /* Image.swift */; }; 962D72BD2CF6436700F86EF0 /* Image+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */; }; 962D72BF2CF7538800F86EF0 /* CGImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */; }; - 962D72C12CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */; }; 962D72C52CF7806300F86EF0 /* ImageReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C42CF7806300F86EF0 /* ImageReflectionTests.swift */; }; 962D72C72CF7815300F86EF0 /* ReflectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */; }; 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */; }; @@ -2764,7 +2763,6 @@ 962D72BA2CF6436600F86EF0 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+Reflection.swift"; sourceTree = ""; }; 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+SessionReplay.swift"; sourceTree = ""; }; - 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwfitUIImage+SessionReplay.swift"; sourceTree = ""; }; 962D72C42CF7806300F86EF0 /* ImageReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageReflectionTests.swift; sourceTree = ""; }; 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflectionMocks.swift; sourceTree = ""; }; 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayPrivacyOverrides.swift; sourceTree = ""; }; @@ -3691,7 +3689,6 @@ children = ( 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */, 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */, - 962D72C02CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift */, D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */, 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, @@ -8471,7 +8468,6 @@ 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */, 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */, 61054E982A6EE10A00AAA894 /* RecordsBuilder.swift in Sources */, - 962D72C12CF7541A00F86EF0 /* SwfitUIImage+SessionReplay.swift in Sources */, 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */, 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */, 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */, diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift index 09faa3f35e..d04b319022 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift @@ -11,12 +11,13 @@ import CoreGraphics /// Icons and small assets usually have dimensions <= 100 points. internal extension CGImage { func isLikelyBundled(scale: CGFloat) -> Bool { - let renderedSize = self.renderedSize(scale: scale) + let pointSize = self.pointSize(scale: scale) let maxDimension: CGFloat = 100 - return renderedSize.width <= maxDimension && renderedSize.height <= maxDimension + print("Point size:", pointSize) + return pointSize.width <= maxDimension && pointSize.height <= maxDimension } - fileprivate func renderedSize(scale: CGFloat) -> CGSize { + private func pointSize(scale: CGFloat) -> CGSize { return CGSize(width: CGFloat(width) / scale, height: CGFloat(height) / scale) } } diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift deleted file mode 100644 index 5e2758f333..0000000000 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/SwfitUIImage+SessionReplay.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -#if os(iOS) -import SwiftUI - -/// Mapping SwiftUI orientation to UIImage orientation -@available(iOS 13.0, tvOS 13.0, *) -internal extension SwiftUI.Image.Orientation { - var uiImageOrientation: UIImage.Orientation { - switch self { - case .up: return .up - case .down: return .down - case .left: return .left - case .right: return .right - case .upMirrored: return .upMirrored - case .downMirrored: return .downMirrored - case .leftMirrored: return .leftMirrored - case .rightMirrored: return .rightMirrored - } - } -} -#endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift index 9f1f0ff1bf..46903d0f11 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/Image.swift @@ -21,16 +21,19 @@ internal struct GraphicsImage { } } +/// Mapping SwiftUI orientation to UIImage orientation @available(iOS 13.0, tvOS 13.0, *) -extension GraphicsImage.Contents: Equatable { - static func == (lhs: GraphicsImage.Contents, rhs: GraphicsImage.Contents) -> Bool { - switch (lhs, rhs) { - case let (.cgImage(lImage), .cgImage(rImage)): - return lImage === rImage - case (.unknown, .unknown): - return true - default: - return false +internal extension UIImage.Orientation { + init(_ orientation: SwiftUI.Image.Orientation) { + switch orientation { + case .up: self = UIImage.Orientation.up + case .down: self = .down + case .left: self = .left + case .right: self = .right + case .upMirrored: self = .upMirrored + case .downMirrored: self = .downMirrored + case .leftMirrored: self = .leftMirrored + case .rightMirrored: self = .rightMirrored } } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift index 75a86debb1..761282e4cf 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/SwiftUI/SwiftUIWireframesBuilder.swift @@ -141,7 +141,7 @@ internal struct SwiftUIWireframesBuilder: NodeWireframesBuilder { image: UIImage( cgImage: cgImage, scale: resolvedImage.scale, - orientation: resolvedImage.orientation.uiImageOrientation + orientation: .init(resolvedImage.orientation) ), tintColor: nil ) diff --git a/DatadogSessionReplay/Tests/ImageReflectionTests.swift b/DatadogSessionReplay/Tests/ImageReflectionTests.swift index 2003ce6f96..770319ea28 100644 --- a/DatadogSessionReplay/Tests/ImageReflectionTests.swift +++ b/DatadogSessionReplay/Tests/ImageReflectionTests.swift @@ -73,11 +73,30 @@ class ImageReflectionTests: XCTestCase { } func testCGImageBundlingHeuristic() { - let smallImage: CGImage = MockCGImage.mockWith(width: 100) - XCTAssertTrue(smallImage.isLikelyBundled(scale: 1.0)) + // Given + let smallImageWidth: Int = 100 + let largeImageWidth: Int = 150 - let largeImage: CGImage = MockCGImage.mockWith(width: 150) - XCTAssertFalse(largeImage.isLikelyBundled(scale: 1.0)) + // Then + let scalex1: CGFloat = 1 + let smallImage1x: CGImage = MockCGImage.mockWith(width: smallImageWidth, scale: scalex1) + XCTAssertTrue(smallImage1x.isLikelyBundled(scale: scalex1)) + let largeImage1x: CGImage = MockCGImage.mockWith(width: largeImageWidth, scale: scalex1) + XCTAssertFalse(largeImage1x.isLikelyBundled(scale: scalex1)) + + // Then + let scale2x: CGFloat = 2 + let smallImage2x: CGImage = MockCGImage.mockWith(width: smallImageWidth, scale: scale2x) + XCTAssertTrue(smallImage2x.isLikelyBundled(scale: scale2x)) + let largeImage2x: CGImage = MockCGImage.mockWith(width: largeImageWidth, scale: scale2x) + XCTAssertFalse(largeImage2x.isLikelyBundled(scale: scale2x)) + + // Then + let scale3x: CGFloat = 3 + let smallImage3x: CGImage = MockCGImage.mockWith(width: smallImageWidth, scale: scale3x) + XCTAssertTrue(smallImage3x.isLikelyBundled(scale: scale3x)) + let largeImage3x: CGImage = MockCGImage.mockWith(width: largeImageWidth, scale: scale3x) + XCTAssertFalse(largeImage3x.isLikelyBundled(scale: scale3x)) } } #endif diff --git a/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift b/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift index f1564f640f..7fd8091132 100644 --- a/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/ReflectionMocks.swift @@ -10,6 +10,20 @@ import TestUtilities import SwiftUI import CoreGraphics +@available(iOS 13.0, tvOS 13.0, *) +extension GraphicsImage.Contents: Equatable { + public static func == (lhs: GraphicsImage.Contents, rhs: GraphicsImage.Contents) -> Bool { + switch (lhs, rhs) { + case let (.cgImage(lImage), .cgImage(rImage)): + return lImage === rImage + case (.unknown, .unknown): + return true + default: + return false + } + } +} + private let bytesPerPixel = 4 private extension Int { @@ -36,38 +50,36 @@ struct MockCGImage: RandomMockable { } static func mockWith( - width: Int = .mockImageDimension() + width: Int = .mockImageDimension(), + scale: CGFloat = 1.0 ) -> CGImage { precondition(width > 0, "Width must be greater than 0") - let totalBytes = width * width * bytesPerPixel + let format = UIGraphicsImageRendererFormat() + format.scale = scale + + let renderer = UIGraphicsImageRenderer( + size: CGSize(width: width, height: width), + format: format + ) - // Allocate memory for pixel data - guard let data = malloc(totalBytes) else { - fatalError("Failed to allocate memory for pixel data") + let image = renderer.image { context in + // Fill with a random color + let randomColor = UIColor( + red: CGFloat.random(in: 0...1), + green: CGFloat.random(in: 0...1), + blue: CGFloat.random(in: 0...1), + alpha: 1.0 + ) + randomColor.setFill() + context.fill(CGRect(origin: .zero, size: CGSize(width: width, height: width))) } - // Free the allocated memory when the function exits - defer { free(data) } - - // Fill the allocated memory with dummy data - memset(data, 255, totalBytes) - - let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! - let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue - guard let context = CGContext( - data: data, - width: width, - height: width, - bitsPerComponent: 8, - bytesPerRow: width * bytesPerPixel, - space: colorSpace, - bitmapInfo: bitmapInfo - ) else { - fatalError("Failed to create CGContext for mock CGImage") + guard let cgImage = image.cgImage else { + fatalError("Failed to create CGImage from UIGraphicsImageRenderer") } - return context.makeImage()! + return cgImage } } From 6c6220f86e2297def6f93087a3966fc2dc1acc9d Mon Sep 17 00:00:00 2001 From: Marie Denis <29802155+mariedm@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:45:32 +0100 Subject: [PATCH 3/3] Remove print statement --- .../Sources/Recorder/Utilities/CGImage+SessionReplay.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift index d04b319022..2f27346b75 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/CGImage+SessionReplay.swift @@ -13,7 +13,6 @@ internal extension CGImage { func isLikelyBundled(scale: CGFloat) -> Bool { let pointSize = self.pointSize(scale: scale) let maxDimension: CGFloat = 100 - print("Point size:", pointSize) return pointSize.width <= maxDimension && pointSize.height <= maxDimension }