From 29d6bcc5c2041ff118b30f550ec6ac6d501def13 Mon Sep 17 00:00:00 2001 From: Martin Conte Mac Donell Date: Mon, 4 Nov 2024 16:13:07 -0800 Subject: [PATCH 1/9] Capture pixel screenshots implementation on iOS --- .../source/replay/SessionReplayTarget.swift | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift index 0cc5d908..afc0a256 100644 --- a/platform/swift/source/replay/SessionReplayTarget.swift +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -7,6 +7,7 @@ @_implementationOnly import CapturePassable import Foundation +import UIKit final class SessionReplayTarget { private let queue = DispatchQueue.serial(withLabelSuffix: "ReplayController", target: .default) @@ -41,14 +42,28 @@ extension SessionReplayTarget: CapturePassable.SessionReplayTarget { } func captureScreenshot() { - // TODO: Implement - // DispatchQueue.main.async { [weak self] in - // self?.queue.async { - // self?.logger?.logSessionReplayScreenshot( - // screen: SessionReplayCapture(data: Data()), - // duration: 0 - // ) - // } - // } + DispatchQueue.main.async { + guard let window = UIApplication.shared.sessionReplayWindows().first else { + return + } + + let layer = window.layer + let bounds = UIScreen.main.bounds.size + + self.queue.async { [weak self] in + let start = Uptime() + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + + let renderer = UIGraphicsImageRenderer(size: bounds, format: format) + let jpeg = renderer.jpegData(withCompressionQuality: 0.1) { context in + layer.render(in: context.cgContext) + } + self?.logger?.logSessionReplayScreenshot( + screen: SessionReplayCapture(data: jpeg), + duration: Uptime().timeIntervalSince(start) + ) + } + } } } From 492dc9dcbdf5fb42af66de8f6cdeba35c32b3467 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 08:42:48 -0500 Subject: [PATCH 2/9] add assertion --- platform/swift/source/replay/SessionReplayTarget.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift index afc0a256..cd428e8c 100644 --- a/platform/swift/source/replay/SessionReplayTarget.swift +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -44,6 +44,7 @@ extension SessionReplayTarget: CapturePassable.SessionReplayTarget { func captureScreenshot() { DispatchQueue.main.async { guard let window = UIApplication.shared.sessionReplayWindows().first else { + assertionFailure("no window to take screenshot of") return } From 51cf3e2698e45d07cb139abd8c1697f6899fa348 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 08:53:22 -0500 Subject: [PATCH 3/9] add doc string --- platform/swift/source/replay/SessionReplayTarget.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift index cd428e8c..7dd884ae 100644 --- a/platform/swift/source/replay/SessionReplayTarget.swift +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -44,6 +44,13 @@ extension SessionReplayTarget: CapturePassable.SessionReplayTarget { func captureScreenshot() { DispatchQueue.main.async { guard let window = UIApplication.shared.sessionReplayWindows().first else { + // According to the documentation for + // `CapturePassable.SessionReplayTarget.captureScreenshot()`, this method must emit + // a screenshot log. Without this, the Rust logger will ignore any further screenshot + // capture actions. + // To handle cases where no window is available, an assertion is included here. + // TODO: Add a log to inform the Rust layer if the screenshot log emission fails, allowing it + // to proceed to the next screenshot (if any). assertionFailure("no window to take screenshot of") return } From 80230c274667a823d41081c22d60789b41b0bc1d Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 08:53:42 -0500 Subject: [PATCH 4/9] fix format --- platform/swift/source/replay/SessionReplayTarget.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift index 7dd884ae..8a4dad69 100644 --- a/platform/swift/source/replay/SessionReplayTarget.swift +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -44,7 +44,7 @@ extension SessionReplayTarget: CapturePassable.SessionReplayTarget { func captureScreenshot() { DispatchQueue.main.async { guard let window = UIApplication.shared.sessionReplayWindows().first else { - // According to the documentation for + // According to the documentation for // `CapturePassable.SessionReplayTarget.captureScreenshot()`, this method must emit // a screenshot log. Without this, the Rust logger will ignore any further screenshot // capture actions. From 662710516a7656361cc084a4e0942cc419099eb4 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 11:16:16 -0500 Subject: [PATCH 5/9] emit empty screenshot log if screenshot cannot be taken --- platform/swift/source/CoreLogger.swift | 5 +++-- platform/swift/source/CoreLogging.swift | 4 ++-- .../swift/source/replay/SessionReplayTarget.swift | 12 ++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/platform/swift/source/CoreLogger.swift b/platform/swift/source/CoreLogger.swift index b79eba11..4a6eb447 100644 --- a/platform/swift/source/CoreLogger.swift +++ b/platform/swift/source/CoreLogger.swift @@ -87,9 +87,10 @@ extension CoreLogger: CoreLogging { ) } - func logSessionReplayScreenshot(screen: SessionReplayCapture, duration: TimeInterval) { + func logSessionReplayScreenshot(screen: SessionReplayCapture?, duration: TimeInterval) { + let fields = screen.flatMap { screen in self.convertFields(fields: ["screen": screen]) } ?? [] self.underlyingLogger.logSessionReplayScreenshot( - fields: self.convertFields(fields: ["screen": screen]), + fields: fields, duration: duration ) } diff --git a/platform/swift/source/CoreLogging.swift b/platform/swift/source/CoreLogging.swift index 03695d2d..700f7fc4 100644 --- a/platform/swift/source/CoreLogging.swift +++ b/platform/swift/source/CoreLogging.swift @@ -55,9 +55,9 @@ protocol CoreLogging: AnyObject { /// Writes a session replay screen log. /// - /// - parameter screen: The captured screenshot. + /// - parameter screen: The captured screenshot. `nil` if screenshot couldn't be taken. /// - parameter duration: The duration of time the preparation of the log took. - func logSessionReplayScreenshot(screen: SessionReplayCapture, duration: TimeInterval) + func logSessionReplayScreenshot(screen: SessionReplayCapture?, duration: TimeInterval) /// Writes a resource utilization log. /// diff --git a/platform/swift/source/replay/SessionReplayTarget.swift b/platform/swift/source/replay/SessionReplayTarget.swift index 8a4dad69..96997178 100644 --- a/platform/swift/source/replay/SessionReplayTarget.swift +++ b/platform/swift/source/replay/SessionReplayTarget.swift @@ -44,14 +44,10 @@ extension SessionReplayTarget: CapturePassable.SessionReplayTarget { func captureScreenshot() { DispatchQueue.main.async { guard let window = UIApplication.shared.sessionReplayWindows().first else { - // According to the documentation for - // `CapturePassable.SessionReplayTarget.captureScreenshot()`, this method must emit - // a screenshot log. Without this, the Rust logger will ignore any further screenshot - // capture actions. - // To handle cases where no window is available, an assertion is included here. - // TODO: Add a log to inform the Rust layer if the screenshot log emission fails, allowing it - // to proceed to the next screenshot (if any). - assertionFailure("no window to take screenshot of") + self.logger?.logSessionReplayScreenshot( + screen: nil, + duration: 0 + ) return } From 8bf82b2ae93aed02f041c0ddf52292c9e0910778 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 11:25:44 -0500 Subject: [PATCH 6/9] fix test --- .../swift/unit_integration/mocks/MockCoreLogging.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift index 34a95214..291c75bf 100644 --- a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift @@ -118,7 +118,9 @@ extension MockCoreLogging: CoreLogging { self.logSessionReplayScreen?.fulfill() } - public func logSessionReplayScreenshot(screen _: SessionReplayCapture, duration _: TimeInterval) {} + public func logSessionReplayScreenshot(screen _: SessionReplayCapture?, duration _: TimeInterval) { + self.logSessionReplayScreenshotExpectation?.fulfill() + } public func logResourceUtilization(fields: Fields, duration: TimeInterval) { self.resourceUtilizationLogs.append(ResourceUtilizationLog(fields: fields, duration: duration)) From 0c8efa402d375486f33d430f658c6a27aa9778b3 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 12:04:58 -0500 Subject: [PATCH 7/9] fix test for real --- .../core/bridge/SessionReplayTargetTests.swift | 2 +- .../swift/unit_integration/mocks/MockCoreLogging.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift b/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift index 75af888b..fcc8743c 100644 --- a/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift +++ b/test/platform/swift/unit_integration/core/bridge/SessionReplayTargetTests.swift @@ -30,7 +30,7 @@ final class SessionReplayTargetTests: XCTestCase { func testEmitsSessionReplayScreenLog() { let expectation = self.expectation(description: "screen log is emitted") - self.logger.logSessionReplayScreen = expectation + self.logger.logSessionReplayScreenExpectation = expectation self.target.captureScreen() XCTAssertEqual(.completed, XCTWaiter().wait(for: [expectation], timeout: 0.5)) diff --git a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift index 291c75bf..d11c55f3 100644 --- a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift @@ -44,7 +44,7 @@ public final class MockCoreLogging { public var logResourceUtilizationExpectation: XCTestExpectation? public private(set) var sessionReplayScreenLogs = [SessionReplayScreenLog]() - public var logSessionReplayScreen: XCTestExpectation? + public var logSessionReplayScreenExpectation: XCTestExpectation? public var shouldLogAppUpdateEvent = false @@ -115,7 +115,7 @@ extension MockCoreLogging: CoreLogging { self.sessionReplayScreenLogs.append(SessionReplayScreenLog( screen: screen, duration: duration) ) - self.logSessionReplayScreen?.fulfill() + self.logSessionReplayScreenExpectation?.fulfill() } public func logSessionReplayScreenshot(screen _: SessionReplayCapture?, duration _: TimeInterval) { From e43ff931d6a0a70754149cd06bf1ea783770f350 Mon Sep 17 00:00:00 2001 From: Rafal Augustyniak Date: Tue, 5 Nov 2024 12:05:19 -0500 Subject: [PATCH 8/9] fix test --- .../swift/unit_integration/mocks/MockCoreLogging.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift index d11c55f3..0b63d80f 100644 --- a/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift +++ b/test/platform/swift/unit_integration/mocks/MockCoreLogging.swift @@ -118,9 +118,7 @@ extension MockCoreLogging: CoreLogging { self.logSessionReplayScreenExpectation?.fulfill() } - public func logSessionReplayScreenshot(screen _: SessionReplayCapture?, duration _: TimeInterval) { - self.logSessionReplayScreenshotExpectation?.fulfill() - } + public func logSessionReplayScreenshot(screen _: SessionReplayCapture?, duration _: TimeInterval) {} public func logResourceUtilization(fields: Fields, duration: TimeInterval) { self.resourceUtilizationLogs.append(ResourceUtilizationLog(fields: fields, duration: duration)) From 5dc579aed44043d17830907667a48b880f558d5c Mon Sep 17 00:00:00 2001 From: Martin Conte Mac Donell Date: Tue, 5 Nov 2024 09:50:19 -0800 Subject: [PATCH 9/9] rename field --- platform/swift/source/CoreLogger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/swift/source/CoreLogger.swift b/platform/swift/source/CoreLogger.swift index 4a6eb447..95c8cb61 100644 --- a/platform/swift/source/CoreLogger.swift +++ b/platform/swift/source/CoreLogger.swift @@ -88,7 +88,7 @@ extension CoreLogger: CoreLogging { } func logSessionReplayScreenshot(screen: SessionReplayCapture?, duration: TimeInterval) { - let fields = screen.flatMap { screen in self.convertFields(fields: ["screen": screen]) } ?? [] + let fields = screen.flatMap { screen in self.convertFields(fields: ["screen_px": screen]) } ?? [] self.underlyingLogger.logSessionReplayScreenshot( fields: fields, duration: duration