From 9f29f648648b96ed367aa6143b4a7f3d34853fb0 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 6 Mar 2023 16:35:05 +0000 Subject: [PATCH 01/72] REPLAY-1448 Add stepper recorder --- .../NodeRecorders/UIStepperRecorder.swift | 30 +++++++++++++++++++ .../ViewTreeSnapshotBuilder.swift | 1 + 2 files changed, 31 insertions(+) create mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift new file mode 100644 index 0000000000..d49c7d7012 --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import UIKit + +internal struct UIStepperRecorder: NodeRecorder { + func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { + guard let slider = view as? UISlider else { + return nil + } + + guard attributes.isVisible else { + return InvisibleElement.constant + } + let builder = UIStepperWireframesBuilder(wireframeRect: slider.frame) + let node = Node(viewAttributes: attributes, wireframesBuilder: builder) + return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) + } +} + +internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { + var wireframeRect: CGRect + + func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { + return [] + } +} diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index eb7d073537..532e9579b4 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -62,6 +62,7 @@ internal let defaultNodeRecorders: [NodeRecorder] = [ UISwitchRecorder(), UISliderRecorder(), UISegmentRecorder(), + UIStepperRecorder(), UINavigationBarRecorder(), UITabBarRecorder(), UIPickerViewRecorder(), From 5369f9b193e357c681334e14fbe40f1f9240fee4 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 7 Mar 2023 11:12:09 +0000 Subject: [PATCH 02/72] REPLAY-1448 Add custom UIStepper wireframes --- .../NodeRecorders/UIStepperRecorder.swift | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index d49c7d7012..3e3652bf00 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -8,14 +8,22 @@ import UIKit internal struct UIStepperRecorder: NodeRecorder { func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { - guard let slider = view as? UISlider else { + guard let stepper = view as? UIStepper else { return nil } guard attributes.isVisible else { return InvisibleElement.constant } - let builder = UIStepperWireframesBuilder(wireframeRect: slider.frame) + + let stepperFrame = CGRect(origin: attributes.frame.origin, size: stepper.intrinsicContentSize) + let ids = context.ids.nodeIDs(4, for: stepper) + + let builder = UIStepperWireframesBuilder( + wireframeRect: stepperFrame, + cornerRadius: stepper.subviews.first?.layer.cornerRadius ?? 0, + ids: ids + ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) } @@ -23,8 +31,55 @@ internal struct UIStepperRecorder: NodeRecorder { internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { var wireframeRect: CGRect + var cornerRadius: CGFloat + var ids: [Int64] func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { - return [] + let background = builder.createShapeWireframe( + id: ids[0], + frame: wireframeRect, + borderColor: nil, + borderWidth: nil, + backgroundColor: SystemColors.tertiarySystemBackground, + cornerRadius: cornerRadius + ) + let divider = builder.createShapeWireframe( + id: ids[1], + frame: CGRect( + origin: CGPoint(x: wireframeRect.origin.x + 46.5, y: wireframeRect.origin.y + 6), + size: CGSize(width: 1, height: 20) + ), + backgroundColor: SystemColors.placeholderText + ) + let stepButtonFontSize = CGFloat(30) + let stepButtonSize = CGSize(width: stepButtonFontSize, height: stepButtonFontSize) + let stepButtonLeftOffset = wireframeRect.width / 2 - stepButtonSize.width / 2 + let minus = builder.createTextWireframe( + id: ids[2], + frame: CGRect( + origin: CGPoint( + x: wireframeRect.origin.x + stepButtonLeftOffset, + y: wireframeRect.origin.y + ), + size: stepButtonSize + ), + text: "-", + textColor: SystemColors.label, + font: .systemFont(ofSize: stepButtonFontSize) + ) + let plus = builder.createTextWireframe( + id: ids[3], + frame: CGRect( + origin: CGPoint( + x: wireframeRect.origin.x + wireframeRect.width / 2 + stepButtonLeftOffset, + y: wireframeRect.origin.y + ), + size: stepButtonSize + ), + text: "+", + textColor: SystemColors.label, + font: .systemFont(ofSize: stepButtonFontSize) + ) + return [background, divider, minus, plus] } } From 8bdba292193332088d65eead6aec6ecedcb43d96 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 7 Mar 2023 11:40:14 +0000 Subject: [PATCH 03/72] REPLAY-1448 Add button custom font color for disabled --- .../NodeRecorders/UIStepperRecorder.swift | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index 3e3652bf00..bbed1344de 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -22,7 +22,9 @@ internal struct UIStepperRecorder: NodeRecorder { let builder = UIStepperWireframesBuilder( wireframeRect: stepperFrame, cornerRadius: stepper.subviews.first?.layer.cornerRadius ?? 0, - ids: ids + ids: ids, + isMinusEnabled: stepper.value > stepper.minimumValue, + isPlusEnabled: stepper.value < stepper.maximumValue ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) @@ -30,9 +32,11 @@ internal struct UIStepperRecorder: NodeRecorder { } internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { - var wireframeRect: CGRect - var cornerRadius: CGFloat - var ids: [Int64] + let wireframeRect: CGRect + let cornerRadius: CGFloat + let ids: [Int64] + let isMinusEnabled: Bool + let isPlusEnabled: Bool func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { let background = builder.createShapeWireframe( @@ -40,7 +44,7 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { frame: wireframeRect, borderColor: nil, borderWidth: nil, - backgroundColor: SystemColors.tertiarySystemBackground, + backgroundColor: SystemColors.tertiarySystemFill, cornerRadius: cornerRadius ) let divider = builder.createShapeWireframe( @@ -53,19 +57,19 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { ) let stepButtonFontSize = CGFloat(30) let stepButtonSize = CGSize(width: stepButtonFontSize, height: stepButtonFontSize) - let stepButtonLeftOffset = wireframeRect.width / 2 - stepButtonSize.width / 2 + let stepButtonLeftOffset = wireframeRect.width / 4 - stepButtonSize.width / 4 let minus = builder.createTextWireframe( id: ids[2], frame: CGRect( origin: CGPoint( - x: wireframeRect.origin.x + stepButtonLeftOffset, + x: wireframeRect.origin.x + stepButtonLeftOffset - 3, y: wireframeRect.origin.y ), size: stepButtonSize ), - text: "-", - textColor: SystemColors.label, - font: .systemFont(ofSize: stepButtonFontSize) + text: "—", + textColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, + font: .systemFont(ofSize: 30) ) let plus = builder.createTextWireframe( id: ids[3], @@ -77,7 +81,7 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { size: stepButtonSize ), text: "+", - textColor: SystemColors.label, + textColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, font: .systemFont(ofSize: stepButtonFontSize) ) return [background, divider, minus, plus] From b34050957461a90592d9f8d6d547eac8bde48518 Mon Sep 17 00:00:00 2001 From: Rosa Trieu Date: Tue, 7 Mar 2023 08:05:43 -0800 Subject: [PATCH 04/72] stop data collection note --- docs/rum_collection/data_collected.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/rum_collection/data_collected.md b/docs/rum_collection/data_collected.md index d5411dca79..e52e2076fe 100644 --- a/docs/rum_collection/data_collected.md +++ b/docs/rum_collection/data_collected.md @@ -131,7 +131,7 @@ You can enable [tracking user info][2] globally to collect and apply user attrib | `session.initial_view.name` | string | Name of the initial view of the session. | | `session.last_view.url` | string | URL of the last view of the session. | | `session.last_view.name` | string | Name of the last view of the session. | -| `session.ip` | string | IP address of the session extracted from the TCP connection of the intake. | +| `session.ip` | string | IP address of the session extracted from the TCP connection of the intake. If you want to stop collecting this attribute, change the setting in your [application details][5]. | | `session.useragent` | string | System user agent info to interpret device info. | @@ -240,3 +240,4 @@ Before data is uploaded to Datadog, it is stored in cleartext in the cache direc [2]: https://docs.datadoghq.com/real_user_monitoring/ios/advanced_configuration/#track-user-sessions [3]: https://support.apple.com/guide/security/security-of-runtime-process-sec15bfe098e/web [4]: https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence +[5]: https://docs.datadoghq.com/data_security/real_user_monitoring/#ip-address From 18df2abafba20a5269c4f300e53a7cf12f7be7fa Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 7 Mar 2023 16:42:52 +0000 Subject: [PATCH 05/72] REPLAY-1448 Add tests --- .../SRHost/Fixtures/Fixtures.swift | 5 ++ .../SRHost/Fixtures/InputElements.storyboard | 76 +++++++++++++++++++ .../SRSnapshotTests/SRSnapshotTests.swift | 18 +++++ .../UIStepperRecorderTests.swift | 47 ++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift index e3a8b4a65f..089f33c6fb 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift @@ -14,6 +14,7 @@ internal enum Fixture: CaseIterable { case pickers case switches case textFields + case steppers var menuItemTitle: String { switch self { @@ -31,6 +32,8 @@ internal enum Fixture: CaseIterable { return "Switches" case .textFields: return "Text Fields" + case .steppers: + return "Steppers" } } @@ -50,6 +53,8 @@ internal enum Fixture: CaseIterable { return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Switches") case .textFields: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "TextFields") + case .textFields: + return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Steppers") } } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard index 741543cead..f6c6e36d38 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard @@ -467,6 +467,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index 9d73e4bdbc..11c134daa3 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -124,4 +124,22 @@ final class SRSnapshotTests: SnapshotTestCase { record: recordingMode ) } + + func testSteppers() throws { + show(fixture: .switches) + + var image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-maskAll-privacy"), + record: recordingMode + ) + } } diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift new file mode 100644 index 0000000000..009d5ede4a --- /dev/null +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift @@ -0,0 +1,47 @@ +/* + * 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. + */ + +import XCTest +@testable import DatadogSessionReplay + +class UIStepperRecorderTests: XCTestCase { + private let recorder = UIStepperRecorder() + private let stepper = UIStepper() + /// `ViewAttributes` simulating common attributes of switch's `UIView`. + private var viewAttributes: ViewAttributes = .mockAny() + + func testWhenStepperIsNotVisible() throws { + // When + viewAttributes = .mock(fixture: .invisible) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: stepper, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is InvisibleElement) + } + + func testWhenStepperIsVisible() throws { + // Given + stepper.tintColor = .mockRandom() + + // When + viewAttributes = .mock(fixture: .visible()) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: stepper, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is SpecificElement) + XCTAssertEqual(semantics.subtreeStrategy, .ignore, "Stepper's subtree should not be recorded") + + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIStepperWireframesBuilder) + } + + func testWhenViewIsNotOfExpectedType() { + // When + let view = UITextField() + + // Then + XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) + } +} From 9516c8ec28d6f24425d03129cb43016a5713b112 Mon Sep 17 00:00:00 2001 From: Rosa Trieu Date: Tue, 7 Mar 2023 09:53:06 -0800 Subject: [PATCH 06/72] Note about how to stop collecting geolocation data --- docs/rum_collection/data_collected.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/rum_collection/data_collected.md b/docs/rum_collection/data_collected.md index e52e2076fe..749b04266e 100644 --- a/docs/rum_collection/data_collected.md +++ b/docs/rum_collection/data_collected.md @@ -83,7 +83,9 @@ The following OS-related attributes are attached automatically to all events col ### Geo-location -The following attributes are related to the geo-location of IP addresses: +The below attributes are related to the geo-location of IP addresses. + +**Note:** If you want to stop collecting geo-location attributes, change the setting in your [application details][6]. | Fullname | Type | Description | |------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------| @@ -241,3 +243,4 @@ Before data is uploaded to Datadog, it is stored in cleartext in the cache direc [3]: https://support.apple.com/guide/security/security-of-runtime-process-sec15bfe098e/web [4]: https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence [5]: https://docs.datadoghq.com/data_security/real_user_monitoring/#ip-address +[6]: https://docs.datadoghq.com/data_security/real_user_monitoring/#geolocation \ No newline at end of file From bedcecb6125022762f2ba82e3702ae354f73c30b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 8 Mar 2023 12:08:32 +0000 Subject: [PATCH 07/72] REPLAY-1448 Refactor to shape elements --- .../SRHost/Fixtures/Fixtures.swift | 2 +- .../SRHost/Fixtures/InputElements.storyboard | 2 +- .../SRSnapshotTests/SRSnapshotTests.swift | 2 +- .../NodeRecorders/UIStepperRecorder.swift | 54 ++++++++++++------- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift index 089f33c6fb..0e4efa7d95 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift @@ -53,7 +53,7 @@ internal enum Fixture: CaseIterable { return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Switches") case .textFields: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "TextFields") - case .textFields: + case .steppers: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Steppers") } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard index f6c6e36d38..45bf4b804f 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements.storyboard @@ -470,7 +470,7 @@ - + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index 11c134daa3..57e23c77d1 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -126,7 +126,7 @@ final class SRSnapshotTests: SnapshotTestCase { } func testSteppers() throws { - show(fixture: .switches) + show(fixture: .steppers) var image = try takeSnapshot(configuration: .init(privacy: .allowAll)) DDAssertSnapshotTest( diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index bbed1344de..13bd383bd6 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -17,7 +17,7 @@ internal struct UIStepperRecorder: NodeRecorder { } let stepperFrame = CGRect(origin: attributes.frame.origin, size: stepper.intrinsicContentSize) - let ids = context.ids.nodeIDs(4, for: stepper) + let ids = context.ids.nodeIDs(5, for: stepper) let builder = UIStepperWireframesBuilder( wireframeRect: stepperFrame, @@ -50,40 +50,54 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let divider = builder.createShapeWireframe( id: ids[1], frame: CGRect( - origin: CGPoint(x: wireframeRect.origin.x + 46.5, y: wireframeRect.origin.y + 6), + origin: CGPoint(x: wireframeRect.origin.x + wireframeRect.size.width / 2, y: wireframeRect.origin.y + 6), size: CGSize(width: 1, height: 20) ), backgroundColor: SystemColors.placeholderText ) - let stepButtonFontSize = CGFloat(30) - let stepButtonSize = CGSize(width: stepButtonFontSize, height: stepButtonFontSize) - let stepButtonLeftOffset = wireframeRect.width / 4 - stepButtonSize.width / 4 - let minus = builder.createTextWireframe( + + + let horizontalElementSize = CGSize(width: 15, height: 1.5) + let verticalElementSize = CGSize(width: 1.5, height: 15) + let horizontalLeftOffset: CGFloat = wireframeRect.size.width / 4 - horizontalElementSize.width / 2 + let verticalLeftOffset: CGFloat = horizontalLeftOffset + horizontalElementSize.width / 2 - verticalElementSize.width / 2 + + let minus = builder.createShapeWireframe( id: ids[2], frame: CGRect( origin: CGPoint( - x: wireframeRect.origin.x + stepButtonLeftOffset - 3, - y: wireframeRect.origin.y + x: wireframeRect.origin.x + horizontalLeftOffset, + y: wireframeRect.origin.y + wireframeRect.height / 2 - horizontalElementSize.height ), - size: stepButtonSize + size: horizontalElementSize ), - text: "—", - textColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, - font: .systemFont(ofSize: 30) + backgroundColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, + cornerRadius: horizontalElementSize.height ) - let plus = builder.createTextWireframe( + let plusHorizontal = builder.createShapeWireframe( id: ids[3], frame: CGRect( origin: CGPoint( - x: wireframeRect.origin.x + wireframeRect.width / 2 + stepButtonLeftOffset, - y: wireframeRect.origin.y + x: wireframeRect.origin.x + wireframeRect.width / 2 + horizontalLeftOffset - 0.5, + y: wireframeRect.origin.y + wireframeRect.height / 2 - horizontalElementSize.height + ), + size: horizontalElementSize + ), + backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, + cornerRadius: horizontalElementSize.height + ) + let plusVertical = builder.createShapeWireframe( + id: ids[4], + frame: CGRect( + origin: CGPoint( + x: wireframeRect.origin.x + wireframeRect.width / 2 + verticalLeftOffset, + y: wireframeRect.origin.y + wireframeRect.height / 2 - verticalElementSize.height / 2 + 0.5 ), - size: stepButtonSize + size: verticalElementSize ), - text: "+", - textColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, - font: .systemFont(ofSize: stepButtonFontSize) + backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, + cornerRadius: verticalElementSize.width ) - return [background, divider, minus, plus] + return [background, divider, minus, plusHorizontal, plusVertical] } } From 7f68e4f31e3b03442a285289909e9f125085b8fd Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 8 Mar 2023 12:23:28 +0000 Subject: [PATCH 08/72] REPLAY-1448 Refactor to generic frame calculation --- .../NodeRecorders/UIStepperRecorder.swift | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index 13bd383bd6..d096715853 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -47,56 +47,43 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { backgroundColor: SystemColors.tertiarySystemFill, cornerRadius: cornerRadius ) + let verticalMargin: CGFloat = 6 let divider = builder.createShapeWireframe( id: ids[1], frame: CGRect( - origin: CGPoint(x: wireframeRect.origin.x + wireframeRect.size.width / 2, y: wireframeRect.origin.y + 6), - size: CGSize(width: 1, height: 20) - ), + origin: CGPoint(x: 0, y: verticalMargin), + size: CGSize(width: 1, height: wireframeRect.size.height - 2 * verticalMargin) + ).putInside(wireframeRect, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: SystemColors.placeholderText ) - - let horizontalElementSize = CGSize(width: 15, height: 1.5) - let verticalElementSize = CGSize(width: 1.5, height: 15) - let horizontalLeftOffset: CGFloat = wireframeRect.size.width / 4 - horizontalElementSize.width / 2 - let verticalLeftOffset: CGFloat = horizontalLeftOffset + horizontalElementSize.width / 2 - verticalElementSize.width / 2 - + let horizontalElementRect = CGRect(origin: .zero, size: CGSize(width: 14, height: 2)) + let verticalElementRect = CGRect(origin: .zero, size: CGSize(width: 2, height: 14)) + let leftButtonFrame = CGRect( + origin: wireframeRect.origin, + size: CGSize(width: wireframeRect.size.width / 2, height: wireframeRect.size.height) + ) + let rightButtonFrame = CGRect( + origin: CGPoint(x: wireframeRect.origin.x + wireframeRect.size.width / 2, y: wireframeRect.origin.y), + size: CGSize(width: wireframeRect.size.width / 2, height: wireframeRect.size.height) + ) let minus = builder.createShapeWireframe( id: ids[2], - frame: CGRect( - origin: CGPoint( - x: wireframeRect.origin.x + horizontalLeftOffset, - y: wireframeRect.origin.y + wireframeRect.height / 2 - horizontalElementSize.height - ), - size: horizontalElementSize - ), + frame: horizontalElementRect.putInside(leftButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, - cornerRadius: horizontalElementSize.height + cornerRadius: horizontalElementRect.size.height ) let plusHorizontal = builder.createShapeWireframe( id: ids[3], - frame: CGRect( - origin: CGPoint( - x: wireframeRect.origin.x + wireframeRect.width / 2 + horizontalLeftOffset - 0.5, - y: wireframeRect.origin.y + wireframeRect.height / 2 - horizontalElementSize.height - ), - size: horizontalElementSize - ), + frame: horizontalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, - cornerRadius: horizontalElementSize.height + cornerRadius: horizontalElementRect.size.height ) let plusVertical = builder.createShapeWireframe( id: ids[4], - frame: CGRect( - origin: CGPoint( - x: wireframeRect.origin.x + wireframeRect.width / 2 + verticalLeftOffset, - y: wireframeRect.origin.y + wireframeRect.height / 2 - verticalElementSize.height / 2 + 0.5 - ), - size: verticalElementSize - ), + frame: verticalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, - cornerRadius: verticalElementSize.width + cornerRadius: verticalElementRect.size.width ) return [background, divider, minus, plusHorizontal, plusVertical] } From c6bdb34ceb6c744ccb4dcd5384755cb112d6432f Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 8 Mar 2023 12:28:07 +0000 Subject: [PATCH 09/72] REPLAY-1448 Fix linter issues --- .../ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index d096715853..90e600392c 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -11,7 +11,6 @@ internal struct UIStepperRecorder: NodeRecorder { guard let stepper = view as? UIStepper else { return nil } - guard attributes.isVisible else { return InvisibleElement.constant } From a0b8aab04f3462ad786b01bfe047f6602fcd715a Mon Sep 17 00:00:00 2001 From: Nacho Bonafonte Date: Thu, 9 Mar 2023 14:50:31 +0100 Subject: [PATCH 10/72] Update to version 2.2.4 of testing framework --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9f3e2c5476..cc694b764c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: dependencies xcodeproj-httpservermock templates # The release version of `dd-sdk-swift-testing` to use for tests instrumentation. -DD_SDK_SWIFT_TESTING_VERSION = 2.2.3 +DD_SDK_SWIFT_TESTING_VERSION = 2.2.4 define DD_SDK_TESTING_XCCONFIG_CI FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]=$$(inherited) $$(SRCROOT)/../instrumented-tests/DatadogSDKTesting.xcframework/ios-arm64_x86_64-simulator/\n From 95de6c6bddbbd972f12c39a89a2e16f8da4312e7 Mon Sep 17 00:00:00 2001 From: Rosa Trieu Date: Thu, 9 Mar 2023 15:29:45 -0800 Subject: [PATCH 11/72] update image --- docs/rum_collection/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rum_collection/_index.md b/docs/rum_collection/_index.md index 99fa978114..5f424d692d 100644 --- a/docs/rum_collection/_index.md +++ b/docs/rum_collection/_index.md @@ -51,7 +51,7 @@ Datadog Real User Monitoring (RUM) enables you to visualize and analyze the real 3. To instrument your web views, click the **Instrument your webviews** toggle. For more information, see [Web View Tracking][12]. 4. To disable automatic user data collection for either client IP or geolocation data, uncheck the boxes for those settings. For more information, see [RUM iOS Data Collected][14]. - {{< img src="real_user_monitoring/ios/new-rum-app-ios.png" alt="Create a RUM application for iOS in Datadog" style="width:100%;border:none" >}} + {{< img src="real_user_monitoring/ios/ios-create-application.png" alt="Create a RUM application for iOS in Datadog" style="width:100%;border:none" >}} To ensure the safety of your data, you must use a client token. If you used only [Datadog API keys][6] to configure the `dd-sdk-ios` library, they would be exposed client-side in the iOS application's byte code. From 29da3043a78a94c097dcf6bec337af2784dba702 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 10 Mar 2023 10:52:46 +0000 Subject: [PATCH 12/72] REPLAY-1448 PR fixes --- .../NodeRecorders/UIStepperRecorder.swift | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index 90e600392c..40fc7eb2bf 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -21,7 +21,11 @@ internal struct UIStepperRecorder: NodeRecorder { let builder = UIStepperWireframesBuilder( wireframeRect: stepperFrame, cornerRadius: stepper.subviews.first?.layer.cornerRadius ?? 0, - ids: ids, + backgroundWireframeID: ids[0], + dividerWireframeID: ids[1], + minusWireframeID: ids[2], + plusHorizontalWireframeID: ids[3], + plusVerticalWireframeID: ids[4], isMinusEnabled: stepper.value > stepper.minimumValue, isPlusEnabled: stepper.value < stepper.maximumValue ) @@ -33,13 +37,17 @@ internal struct UIStepperRecorder: NodeRecorder { internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let wireframeRect: CGRect let cornerRadius: CGFloat - let ids: [Int64] + let backgroundWireframeID: WireframeID + let dividerWireframeID: WireframeID + let minusWireframeID: WireframeID + let plusHorizontalWireframeID: WireframeID + let plusVerticalWireframeID: WireframeID let isMinusEnabled: Bool let isPlusEnabled: Bool func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { let background = builder.createShapeWireframe( - id: ids[0], + id: backgroundWireframeID, frame: wireframeRect, borderColor: nil, borderWidth: nil, @@ -48,7 +56,7 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { ) let verticalMargin: CGFloat = 6 let divider = builder.createShapeWireframe( - id: ids[1], + id: dividerWireframeID, frame: CGRect( origin: CGPoint(x: 0, y: verticalMargin), size: CGSize(width: 1, height: wireframeRect.size.height - 2 * verticalMargin) @@ -58,28 +66,21 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let horizontalElementRect = CGRect(origin: .zero, size: CGSize(width: 14, height: 2)) let verticalElementRect = CGRect(origin: .zero, size: CGSize(width: 2, height: 14)) - let leftButtonFrame = CGRect( - origin: wireframeRect.origin, - size: CGSize(width: wireframeRect.size.width / 2, height: wireframeRect.size.height) - ) - let rightButtonFrame = CGRect( - origin: CGPoint(x: wireframeRect.origin.x + wireframeRect.size.width / 2, y: wireframeRect.origin.y), - size: CGSize(width: wireframeRect.size.width / 2, height: wireframeRect.size.height) - ) + let (leftButtonFrame, rightButtonFrame) = wireframeRect.divided(atDistance: wireframeRect.size.width / 2, from: .minXEdge) let minus = builder.createShapeWireframe( - id: ids[2], + id: minusWireframeID, frame: horizontalElementRect.putInside(leftButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: horizontalElementRect.size.height ) let plusHorizontal = builder.createShapeWireframe( - id: ids[3], + id: plusHorizontalWireframeID, frame: horizontalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: horizontalElementRect.size.height ) let plusVertical = builder.createShapeWireframe( - id: ids[4], + id: plusVerticalWireframeID, frame: verticalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: verticalElementRect.size.width From e37bd0b6234fe764637bbccdc9d51d4b31bcc99a Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 8 Mar 2023 13:38:41 +0100 Subject: [PATCH 13/72] REPLAY-1330 Add snapshot tests for `UIDatePicker` --- .../SRSnapshotTests/SRHost/AppDelegate.swift | 18 +- .../SRHost/Fixtures/Fixtures.swift | 31 +++ .../InputElements-DatePickers.storyboard | 235 ++++++++++++++++++ .../Fixtures/InputViewControllers.swift | 64 +++++ .../SRHost/MenuViewController.swift | 14 +- .../SRSnapshotTests.xcodeproj/project.pbxproj | 8 + .../SRSnapshotTests/SRSnapshotTests.swift | 114 +++++++++ .../Utils/SnapshotTestCase.swift | 8 + 8 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements-DatePickers.storyboard diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/AppDelegate.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/AppDelegate.swift index 8896ace1d1..da5061988c 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/AppDelegate.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/AppDelegate.swift @@ -34,10 +34,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } var keyWindow: UIWindow? { - return UIApplication.shared - .connectedScenes - .compactMap { $0 as? UIWindowScene } - .first { scene in scene.windows.contains { window in window.isKeyWindow } }? - .keyWindow + if #available(iOS 15.0, *) { + return UIApplication.shared + .connectedScenes + .compactMap { $0 as? UIWindowScene } + .first { scene in scene.windows.contains { window in window.isKeyWindow } }? + .keyWindow + } else { + let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication // swiftlint:disable:this unsafe_uiapplication_shared + return application? + .connectedScenes + .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } + .first { $0.isKeyWindow } + } } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift index 0e4efa7d95..caebe8d305 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift @@ -15,6 +15,12 @@ internal enum Fixture: CaseIterable { case switches case textFields case steppers + case datePickersInline + case datePickersCompact + case datePickersWheels + case timePickersCountDown + case timePickersWheels + case timePickersCompact var menuItemTitle: String { switch self { @@ -34,6 +40,18 @@ internal enum Fixture: CaseIterable { return "Text Fields" case .steppers: return "Steppers" + case .datePickersInline: + return "Date Picker (inline)" + case .datePickersCompact: + return "Date Picker (compact)" + case .datePickersWheels: + return "Date Picker (wheels)" + case .timePickersCountDown: + return "Time Picker (count down)" + case .timePickersWheels: + return "Time Picker (wheels)" + case .timePickersCompact: + return "Time Picker (compact)" } } @@ -55,6 +73,18 @@ internal enum Fixture: CaseIterable { return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "TextFields") case .steppers: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Steppers") + case .datePickersInline: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "DatePickersInline") + case .datePickersCompact: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "DatePickersCompact") + case .datePickersWheels: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "DatePickersWheels") + case .timePickersCountDown: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "TimePickersCountDown") + case .timePickersWheels: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "TimePickersWheels") + case .timePickersCompact: + return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "DatePickersCompact") // sharing the same VC with `datePickersCompact` } } } @@ -62,4 +92,5 @@ internal enum Fixture: CaseIterable { internal extension UIStoryboard { static var basic: UIStoryboard { UIStoryboard(name: "Basic", bundle: nil) } static var inputElements: UIStoryboard { UIStoryboard(name: "InputElements", bundle: nil) } + static var datePickers: UIStoryboard { UIStoryboard(name: "InputElements-DatePickers", bundle: nil) } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements-DatePickers.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements-DatePickers.storyboard new file mode 100644 index 0000000000..152710ac75 --- /dev/null +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputElements-DatePickers.storyboard @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputViewControllers.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputViewControllers.swift index 6866edfb00..20e4d64912 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputViewControllers.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/InputViewControllers.swift @@ -57,3 +57,67 @@ internal class PickersViewController: UIViewController { secondPicker.selectRow(4, inComponent: 2, animated: false) } } + +internal class DatePickersInlineViewController: UIViewController { + @IBOutlet weak var datePicker: UIDatePicker! + + func set(date: Date) { + datePicker.setDate(date, animated: false) + } +} + +internal class DatePickersCompactViewController: UIViewController { + @IBOutlet weak var datePicker: UIDatePicker! + + func set(date: Date) { + datePicker.setDate(date, animated: false) + } + + /// Forces the "compact" date picker to open full calendar view in a popover. + func openCalendarPopover() { + // Here we use private Objc APIs. It works fine on iOS 15.0+ which matches the OS version used + // for snapshot tests, but might need updates in the future. + if #available(iOS 15.0, *) { + let label = datePicker.subviews[0].subviews[0] + let tapAction = NSSelectorFromString("_didTapTextLabel") + label.perform(tapAction) + } + } + + /// Forces the "wheel" time picker to open in a popover. + func openTimePickerPopover() { + // Here we use private Objc APIs - it works fine on iOS 15.0+ which matches the OS version used + // for snapshot tests, but might need updates in the future. + if #available(iOS 15.0, *) { + class DummySender: NSObject { + @objc + func activeTouch() -> UITouch? { return nil } + } + + let label = datePicker.subviews[0].subviews[1] + let tapAction = NSSelectorFromString("didTapInputLabel:") + label.perform(tapAction, with: DummySender()) + } + } +} + +internal class DatePickersWheelsViewController: UIViewController { + @IBOutlet weak var datePicker: UIDatePicker! + + func set(date: Date) { + datePicker.setDate(date, animated: false) + } +} + +internal class TimePickersCountDownViewController: UIViewController {} + +internal class TimePickersWheelViewController: UIViewController { + @IBOutlet weak var datePicker: UIDatePicker! + + func set(date: Date) { + datePicker.setDate(date, animated: false) + } +} + +/// Sharing the same VC for compact time and date picker. +internal typealias TimePickersCompactViewController = DatePickersCompactViewController diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/MenuViewController.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/MenuViewController.swift index a9c22dbfee..62601fcf5e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/MenuViewController.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/MenuViewController.swift @@ -17,9 +17,17 @@ internal class MenuViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell() - var content = cell.defaultContentConfiguration() - content.text = Fixture.allCases[indexPath.item].menuItemTitle - cell.contentConfiguration = content + + if #available(iOS 14.0, *) { + var content = cell.defaultContentConfiguration() + content.text = Fixture.allCases[indexPath.item].menuItemTitle + cell.contentConfiguration = content + } else { + let label = UILabel(frame: .init(x: 10, y: 0, width: tableView.bounds.width, height: 44)) + label.text = Fixture.allCases[indexPath.item].menuItemTitle + cell.addSubview(label) + } + return cell } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index 77a1ae7a58..4fdcc08e3f 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 616C37DA299F6913005E0472 /* InputElements.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 616C37D9299F6913005E0472 /* InputElements.storyboard */; }; + 6196D32429AF7EB2002EACAF /* InputElements-DatePickers.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6196D32329AF7EB2002EACAF /* InputElements-DatePickers.storyboard */; }; 619C49B229952108006B66A6 /* Framer in Frameworks */ = {isa = PBXBuildFile; productRef = 619C49B129952108006B66A6 /* Framer */; }; 619C49B429952E12006B66A6 /* SnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619C49B329952E12006B66A6 /* SnapshotTestCase.swift */; }; 619C49B72995512A006B66A6 /* ImageComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619C49B62995512A006B66A6 /* ImageComparison.swift */; }; @@ -39,6 +40,7 @@ /* Begin PBXFileReference section */ 616C37D9299F6913005E0472 /* InputElements.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = InputElements.storyboard; sourceTree = ""; }; + 6196D32329AF7EB2002EACAF /* InputElements-DatePickers.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "InputElements-DatePickers.storyboard"; sourceTree = ""; }; 619C49B329952E12006B66A6 /* SnapshotTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTestCase.swift; sourceTree = ""; }; 619C49B62995512A006B66A6 /* ImageComparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageComparison.swift; sourceTree = ""; }; 619C49B8299551F5006B66A6 /* _snapshots_ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = _snapshots_; sourceTree = ""; }; @@ -147,6 +149,7 @@ 61E7DFBA299A5C9D001D7A3A /* BasicViewControllers.swift */, 616C37D9299F6913005E0472 /* InputElements.storyboard */, 61A735A929A5137400001820 /* InputViewControllers.swift */, + 6196D32329AF7EB2002EACAF /* InputElements-DatePickers.storyboard */, ); path = Fixtures; sourceTree = ""; @@ -246,6 +249,7 @@ 61B3BC572993BE2F0032C78A /* LaunchScreen.storyboard in Resources */, 61E7DFB9299A5A3E001D7A3A /* Basic.storyboard in Resources */, 61B3BC522993BE2E0032C78A /* Main.storyboard in Resources */, + 6196D32429AF7EB2002EACAF /* InputElements-DatePickers.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -439,6 +443,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -466,6 +471,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -490,6 +496,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JKFCB4CN7C; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SRSnapshotTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -507,6 +514,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JKFCB4CN7C; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SRSnapshotTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift index 57e23c77d1..bd3ada0cf6 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/SRSnapshotTests.swift @@ -6,6 +6,7 @@ import XCTest @testable import SRHost +import TestUtilities final class SRSnapshotTests: SnapshotTestCase { private let snapshotsFolderName = "_snapshots_" @@ -142,4 +143,117 @@ final class SRSnapshotTests: SnapshotTestCase { record: recordingMode ) } + + + func testDatePickers() throws { + let vc1 = show(fixture: .datePickersInline) as! DatePickersInlineViewController + vc1.set(date: .mockDecember15th2019At10AMUTC()) + wait(seconds: 0.25) + + var image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-inline-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-inline-maskAll-privacy"), + record: recordingMode + ) + + let vc2 = show(fixture: .datePickersCompact) as! DatePickersCompactViewController + vc2.set(date: .mockDecember15th2019At10AMUTC()) + vc2.openCalendarPopover() + wait(seconds: 0.25) + + image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-compact-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-compact-maskAll-privacy"), + record: recordingMode + ) + + let vc3 = show(fixture: .datePickersWheels) as! DatePickersWheelsViewController + vc3.set(date: .mockDecember15th2019At10AMUTC()) + wait(seconds: 0.25) + + image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-wheels-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-wheels-maskAll-privacy"), + record: recordingMode + ) + } + + func testTimePickers() throws { + show(fixture: .timePickersCountDown) + + var image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-count-down-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-count-down-maskAll-privacy"), + record: recordingMode + ) + + let vc1 = show(fixture: .timePickersWheels) as! TimePickersWheelViewController + vc1.set(date: .mockDecember15th2019At10AMUTC()) + wait(seconds: 0.25) + + image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-wheels-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-wheels-maskAll-privacy"), + record: recordingMode + ) + + let vc2 = show(fixture: .timePickersCompact) as! TimePickersCompactViewController + vc2.set(date: .mockDecember15th2019At10AMUTC()) + vc2.openTimePickerPopover() + wait(seconds: 0.25) + + image = try takeSnapshot(configuration: .init(privacy: .allowAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-compact-allowAll-privacy"), + record: recordingMode + ) + + image = try takeSnapshot(configuration: .init(privacy: .maskAll)) + DDAssertSnapshotTest( + newImage: image, + snapshotLocation: .folder(named: snapshotsFolderName, fileNameSuffix: "-compact-maskAll-privacy"), + record: recordingMode + ) + } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift index 329e4bf9da..533ec62033 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift @@ -63,6 +63,14 @@ internal class SnapshotTestCase: XCTestCase { return createSideBySideImage(appImage, wireframesImage) } + func wait(seconds: TimeInterval) { + let expectation = self.expectation(description: "Wait \(seconds)") + DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { + expectation.fulfill() + } + waitForExpectations(timeout: seconds * 2) + } + /// Puts two images side-by-side, adds titles and returns new, composite image. private func createSideBySideImage(_ image1: UIImage, _ image2: UIImage) -> UIImage { var leftRect = CGRect(origin: .zero, size: image1.size) From 4f9658714e196673c5679aec0f88d9b441b77b46 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 10 Mar 2023 13:01:11 +0100 Subject: [PATCH 14/72] REPLAY-1330 Record `UIDatePickers` --- .../Processor/Privacy/TextObfuscator.swift | 2 +- .../Recorder/Utilities/SystemColors.swift | 18 ++ .../NodeRecorders/UIDatePickerRecorder.swift | 163 ++++++++++++++++++ .../NodeRecorders/UIImageViewRecorder.swift | 8 +- .../NodeRecorders/UILabelRecorder.swift | 13 +- .../NodeRecorders/UIPickerViewRecorder.swift | 52 +++--- .../NodeRecorders/UISegmentRecorder.swift | 2 +- .../NodeRecorders/UITextFieldRecorder.swift | 8 +- .../NodeRecorders/UITextViewRecorder.swift | 2 +- .../NodeRecorders/UIViewRecorder.swift | 8 +- .../ViewTreeSnapshot/ViewTreeRecorder.swift | 6 +- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 13 +- .../ViewTreeSnapshotBuilder.swift | 65 ++++--- .../Tests/Mocks/RecorderMocks.swift | 24 ++- .../Privacy/TextObfuscatorTests.swift | 2 +- .../UIDatePickerRecorderTests.swift | 53 ++++++ .../NodeRecorders/UILabelRecorderTests.swift | 18 +- .../UITextFieldRecorderTests.swift | 32 +++- .../UITextViewRecorderTests.swift | 18 +- .../ViewTreeRecorderTests.swift | 6 +- .../ViewTreeSnapshotBuilderTests.swift | 36 +++- 21 files changed, 443 insertions(+), 106 deletions(-) create mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift create mode 100644 DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift diff --git a/DatadogSessionReplay/Sources/Processor/Privacy/TextObfuscator.swift b/DatadogSessionReplay/Sources/Processor/Privacy/TextObfuscator.swift index 8b94f8b51e..6a7047081d 100644 --- a/DatadogSessionReplay/Sources/Processor/Privacy/TextObfuscator.swift +++ b/DatadogSessionReplay/Sources/Processor/Privacy/TextObfuscator.swift @@ -44,7 +44,7 @@ internal struct TextObfuscator: TextObfuscating { /// It should be used **by default** for input elements that bring sensitive information (such as passwords). /// It shuold be used for input elements that can't safely use space-preserving masking (such as date pickers, where selection can be still /// inferred by counting the number of x-es in the mask). -internal struct InputTextObfuscator: TextObfuscating { +internal struct SensitiveTextObfuscator: TextObfuscating { private static let maskedString = "xxx" func mask(text: String) -> String { Self.maskedString } diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift index 721d397d50..ed4c894e6f 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/SystemColors.swift @@ -34,6 +34,24 @@ internal enum SystemColors { } } + static var systemBackground: CGColor { + if #available(iOS 13.0, *) { + return UIColor.systemBackground.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 255 / 255, green: 255 / 255, blue: 255 / 255, alpha: 1).cgColor + } + } + + static var secondarySystemGroupedBackground: CGColor { + if #available(iOS 13.0, *) { + return UIColor.secondarySystemGroupedBackground.cgColor + } else { + // Fallback to iOS 16.2 light mode color: + return UIColor(red: 255 / 255, green: 255 / 255, blue: 255 / 255, alpha: 1).cgColor + } + } + static var secondarySystemFill: CGColor { if #available(iOS 13.0, *) { return UIColor.secondarySystemFill.cgColor diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift new file mode 100644 index 0000000000..1b08882601 --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift @@ -0,0 +1,163 @@ +/* + * 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. + */ + +import UIKit + +internal struct UIDatePickerRecorder: NodeRecorder { + private let wheelsStyleRecorder = WheelsStyleDatePickerRecorder() + private let compactStyleRecorder = CompactStyleDatePickerRecorder() + private let inlineStyleRecorder = InlineStyleDatePickerRecorder() + + func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { + guard let datePicker = view as? UIDatePicker else { + return nil + } + + guard attributes.isVisible else { + return InvisibleElement.constant + } + + var nodes: [Node] = [] + + if #available(iOS 13.4, *) { + switch datePicker.datePickerStyle { + case .wheels: + nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + case .compact: + nodes = compactStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + case .inline: + nodes = inlineStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + case .automatic: + // According to `datePicker.datePickerStyle` documentation: + // > "This property always returns a concrete style, never `UIDatePickerStyle.automatic`." + break + @unknown default: + nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + } + } else { + // Observation: older OS versions use the "wheels" style + nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + } + + let isDisplayedInPopover: Bool = { + if let superview = view.superview { + // This gets effective on iOS 15.0+ which is the earliest version that displays + // date pickers in popover views: + return "\(type(of: superview))" == "_UIVisualEffectContentView" + } + return false + }() + + let builder = UIDatePickerWireframesBuilder( + wireframeRect: attributes.frame, + attributes: attributes, + backgroundWireframeID: context.ids.nodeID(for: datePicker), + isDisplayedInPopover: isDisplayedInPopover + ) + let backgroundNode = Node( + viewAttributes: attributes, + wireframesBuilder: builder + ) + return SpecificElement( + subtreeStrategy: .ignore, + nodes: [backgroundNode] + nodes + ) + } +} + +private struct WheelsStyleDatePickerRecorder { + let pickerTreeRecorder = ViewTreeRecorder( + nodeRecorders: [ + UIPickerViewRecorder() + ] + ) + + func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { + var context = context + // Do not elevate text obfuscation for selected options in the context of date picker. + // Meaning: do not replace dates with fixed-width `"xxx"` mask - instead, replace each character individually. + context.selectionTextObfuscator = context.textObfuscator + return pickerTreeRecorder.recordNodes(for: view, in: context) + } +} + +private struct InlineStyleDatePickerRecorder { + let viewRecorder: UIViewRecorder + let labelRecorder: UILabelRecorder + let subtreeRecorder: ViewTreeRecorder + + init() { + self.viewRecorder = UIViewRecorder() + self.labelRecorder = UILabelRecorder() + self.subtreeRecorder = ViewTreeRecorder( + nodeRecorders: [ + viewRecorder, + labelRecorder, + UIImageViewRecorder(), + UISegmentRecorder(), // iOS 14.x uses `UISegmentedControl` for "AM | PM" + ] + ) + } + + func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { + viewRecorder.semanticsOverride = { _, viewAttributes in + if context.recorder.privacy == .maskAll { + let isSquare = viewAttributes.frame.width == viewAttributes.frame.height + let isCircle = isSquare && viewAttributes.layerCornerRadius == viewAttributes.frame.width * 0.5 + if isCircle { + return IgnoredElement(subtreeStrategy: .ignore) + } + } + return nil + } + + if context.recorder.privacy == .maskAll { + labelRecorder.builderOverride = { builder in + var builder = builder + builder.textColor = SystemColors.label + return builder + } + } + + return subtreeRecorder.recordNodes(for: view, in: context) + } +} + +private struct CompactStyleDatePickerRecorder { + let subtreeRecorder = ViewTreeRecorder( + nodeRecorders: [ + UIViewRecorder(), + UILabelRecorder() + ] + ) + + func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { + return subtreeRecorder.recordNodes(for: view, in: context) + } +} + +internal struct UIDatePickerWireframesBuilder: NodeWireframesBuilder { + var wireframeRect: CGRect + let attributes: ViewAttributes + let backgroundWireframeID: WireframeID + /// If date picker is displayed in popover view (possible only in iOS 15.0+). + let isDisplayedInPopover: Bool + + func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { + return [ + builder.createShapeWireframe( + id: backgroundWireframeID, + frame: wireframeRect, + clip: nil, + borderColor: isDisplayedInPopover ? SystemColors.secondarySystemFill : nil, + borderWidth: isDisplayedInPopover ? 1 : 0, + backgroundColor: isDisplayedInPopover ? SystemColors.secondarySystemGroupedBackground : SystemColors.systemBackground, + cornerRadius: 10, + opacity: attributes.alpha + ) + ] + } +} diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 5e9fa9b6f6..3e0e48dcbb 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -6,7 +6,10 @@ import UIKit -internal struct UIImageViewRecorder: NodeRecorder { +internal class UIImageViewRecorder: NodeRecorder { + /// An option for overriding default semantics from parent recorder. + var semanticsOverride: (UIImageView, ViewAttributes) -> NodeSemantics? = { _, _ in nil } + private let imageDataProvider = ImageDataProvider() func semantics( @@ -17,6 +20,9 @@ internal struct UIImageViewRecorder: NodeRecorder { guard let imageView = view as? UIImageView else { return nil } + if let semantics = semanticsOverride(imageView, attributes) { + return semantics + } guard attributes.hasAnyAppearance || imageView.image != nil else { return InvisibleElement.constant } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift index 7b01202e3f..1955a20538 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift @@ -7,8 +7,6 @@ import UIKit internal class UILabelRecorder: NodeRecorder { - /// An option for ignoring certain views by this recorder. - var dropPredicate: (UILabel, ViewAttributes) -> Bool = { _, _ in false } /// An option for customizing wireframes builder created by this recorder. var builderOverride: (UILabelWireframesBuilder) -> UILabelWireframesBuilder = { $0 } @@ -16,11 +14,8 @@ internal class UILabelRecorder: NodeRecorder { guard let label = view as? UILabel else { return nil } - if dropPredicate(label, attributes) { - return nil - } - let hasVisibleText = !(label.text?.isEmpty ?? true) + let hasVisibleText = attributes.isVisible && !(label.text?.isEmpty ?? true) guard hasVisibleText || attributes.hasAnyAppearance else { return InvisibleElement.constant @@ -42,7 +37,7 @@ internal class UILabelRecorder: NodeRecorder { textAlignment: nil, font: label.font, fontScalingEnabled: label.adjustsFontSizeToFitWidth, - textObfuscator: context.recorder.privacy == .maskAll ? context.textObfuscator : nopTextObfuscator, + textObfuscator: context.textObfuscator, wireframeRect: textFrame ) let node = Node(viewAttributes: attributes, wireframesBuilder: builderOverride(builder)) @@ -57,13 +52,13 @@ internal struct UILabelWireframesBuilder: NodeWireframesBuilder { /// The text inside label. let text: String /// The color of the text. - let textColor: CGColor? + var textColor: CGColor? /// The alignment of the text. var textAlignment: SRTextPosition.Alignment? /// The font used by the label. let font: UIFont? /// Flag that determines if font should be scaled - let fontScalingEnabled: Bool + var fontScalingEnabled: Bool /// Text obfuscator for masking text. let textObfuscator: TextObfuscating diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift index 32f5ba6956..9740729e8c 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift @@ -13,11 +13,9 @@ import UIKit /// - We can't request `picker.dataSource` to receive the value - doing so will result in calling applicaiton code, which could be /// dangerous (if the code is faulty) and may significantly slow down the performance (e.g. if the underlying source requires database fetch). /// - Similarly, we don't call `picker.delegate` to avoid running application code outside `UIKit's` lifecycle. -/// - Instead, we infer the value by traversing picker's subtree state and finding texts that are displayed closest to its geometry center. +/// - Instead, we infer the value by traversing picker's subtree and finding texts that have no "3D wheel" effect applied. /// - If privacy mode is elevated, we don't replace individual characters with "x" letter - instead we change whole options to fixed-width mask value. internal struct UIPickerViewRecorder: NodeRecorder { - /// Records individual labels in picker's subtree. - private let labelRecorder: UILabelRecorder /// Records all shapes in picker's subtree. /// It is used to capture the background of selected option. private let selectionRecorder: ViewTreeRecorder @@ -26,8 +24,27 @@ internal struct UIPickerViewRecorder: NodeRecorder { private let labelsRecorder: ViewTreeRecorder init() { - self.labelRecorder = UILabelRecorder() - self.labelsRecorder = ViewTreeRecorder(nodeRecorders: [labelRecorder]) + let viewRecorder = UIViewRecorder() + viewRecorder.semanticsOverride = { view, attributes in + if #available(iOS 13.0, *) { + if attributes.isTranslucent || !CATransform3DIsIdentity(view.transform3D) { + // If this view has any 3D effect applied, do not enter its subtree: + return IgnoredElement(subtreeStrategy: .ignore) + } + } + // Otherwise, enter the subtree of this element, but do not consider it significant (`InvisibleElement`): + return InvisibleElement(subtreeStrategy: .record) + } + + let labelRecorder = UILabelRecorder() + labelRecorder.builderOverride = { builder in + var builder = builder + builder.textAlignment = .init(horizontal: .center, vertical: .center) + builder.fontScalingEnabled = true + return builder + } + + self.labelsRecorder = ViewTreeRecorder(nodeRecorders: [viewRecorder, labelRecorder]) self.selectionRecorder = ViewTreeRecorder(nodeRecorders: [UIViewRecorder()]) } @@ -68,25 +85,10 @@ internal struct UIPickerViewRecorder: NodeRecorder { return selectionRecorder.recordNodes(for: picker, in: context) } - /// Records `UILabel` nodes that hold titles of **selected** options - if picker defines N components, there will be N nodes returned. + /// Records `UILabel` nodes that hold titles of **selected** options. private func recordTitlesOfSelectedOption(in picker: UIPickerView, pickerAttributes: ViewAttributes, using context: ViewTreeRecordingContext) -> [Node] { - labelRecorder.dropPredicate = { _, labelAttributes in - // We consider option to be "selected" if it is displayed close enough to picker's geometry center - // and its `UILabel` is opaque: - let isNearCenter = abs(labelAttributes.frame.midY - pickerAttributes.frame.midY) < 10 - let isForeground = labelAttributes.alpha == 1 - let isSelectedOption = isNearCenter && isForeground - return !isSelectedOption // drop other options than selected one - } - - labelRecorder.builderOverride = { builder in - var builder = builder - builder.textAlignment = .init(horizontal: .center, vertical: .center) - return builder - } - var context = context - context.textObfuscator = InputTextObfuscator() + context.textObfuscator = context.selectionTextObfuscator return labelsRecorder.recordNodes(for: picker, in: context) } } @@ -98,11 +100,7 @@ internal struct UIPickerViewWireframesBuilder: NodeWireframesBuilder { func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { return [ - builder.createShapeWireframe( - id: backgroundWireframeID, - frame: wireframeRect, - attributes: attributes - ) + builder.createShapeWireframe(id: backgroundWireframeID, frame: wireframeRect, attributes: attributes) ] } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift index a7c0a299da..d0f2eac084 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift @@ -21,7 +21,7 @@ internal struct UISegmentRecorder: NodeRecorder { let builder = UISegmentWireframesBuilder( wireframeRect: attributes.frame, attributes: attributes, - textObfuscator: context.recorder.privacy == .maskAll ? context.textObfuscator : nopTextObfuscator, + textObfuscator: context.textObfuscator, backgroundWireframeID: ids[0], segmentWireframeIDs: Array(ids[1.. [Node] { - backgroundViewRecorder.dropPredicate = { _, viewAttributes in + backgroundViewRecorder.semanticsOverride = { _, viewAttributes in // We consider view to define text field's appearance if it has the same // size as text field: let hasSameSize = textFieldAttributes.frame == viewAttributes.frame let isBackground = hasSameSize && viewAttributes.hasAnyAppearance - return !isBackground + return !isBackground ? IgnoredElement(subtreeStrategy: .record) : nil } return subtreeRecorder.recordNodes(for: textField, in: context) @@ -88,10 +88,10 @@ internal struct UITextFieldRecorder: NodeRecorder { private func textObfuscator(for textField: UITextField, in context: ViewTreeRecordingContext) -> TextObfuscating { if textField.isSecureTextEntry || textField.textContentType == .emailAddress || textField.textContentType == .telephoneNumber { - return InputTextObfuscator() + return context.sensitiveTextObfuscator } - return context.recorder.privacy == .maskAll ? context.textObfuscator : nopTextObfuscator // default one + return context.textObfuscator } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift index 06fa38e179..cf5f337fc8 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift @@ -20,7 +20,7 @@ internal struct UITextViewRecorder: NodeRecorder { text: textView.text, textColor: textView.textColor?.cgColor ?? UIColor.black.cgColor, font: textView.font, - textObfuscator: context.recorder.privacy == .maskAll ? context.textObfuscator : nopTextObfuscator, + textObfuscator: context.textObfuscator, contentRect: CGRect(origin: textView.contentOffset, size: textView.contentSize) ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift index dea1c2cda5..128bf39d1f 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift @@ -7,15 +7,15 @@ import UIKit internal class UIViewRecorder: NodeRecorder { - /// An option for ignoring certain views by this recorder. - var dropPredicate: (UIView, ViewAttributes) -> Bool = { _, _ in false } + /// An option for overriding default semantics from parent recorder. + var semanticsOverride: (UIView, ViewAttributes) -> NodeSemantics? = { _, _ in nil } func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { guard attributes.isVisible else { return InvisibleElement.constant } - if dropPredicate(view, attributes) { - return nil + if let semantics = semanticsOverride(view, attributes) { + return semantics } guard attributes.hasAnyAppearance else { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift index c9f2101fa3..43e9a91552 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift @@ -16,9 +16,13 @@ internal struct ViewTreeRecordingContext { let coordinateSpace: UICoordinateSpace /// Generates stable IDs for traversed views. let ids: NodeIDGenerator - /// Masks text in recorded nodes. + /// Text obfuscator applied to all non-sensitive texts. No-op if privacy mode is disabled. /// Can be overwriten in by `NodeRecorder` if their subtree recording requires different masking. var textObfuscator: TextObfuscating + /// Text obfuscator applied to user selection texts (such as labels in picker control). + var selectionTextObfuscator: TextObfuscating + /// Text obfuscator applied to all sensitive texts (such as passwords or e-mail address). + let sensitiveTextObfuscator: TextObfuscating } internal struct ViewTreeRecorder { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 7a6741552a..384323783d 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -180,7 +180,8 @@ internal struct UnknownElement: NodeSemantics { /// A semantics of an UI element that is either `UIView` or one of its known subclasses. This semantics mean that the element /// has no visual appearance that can be presented in SR (e.g. a `UILabel` with no text, no border and fully transparent color). -/// Nodes with this semantics can be safely ignored in `Recorder` or in `Processor`. +/// Unlike `IgnoredElement`, this semantics can be overwritten with another one with higher importance. This means that even +/// if the root view of certain element has no appearance, other node recorders will continue checking it for strictkier semantics. internal struct InvisibleElement: NodeSemantics { static let importance: Int = 0 let subtreeStrategy: NodeSubtreeStrategy @@ -195,10 +196,18 @@ internal struct InvisibleElement: NodeSemantics { self.subtreeStrategy = subtreeStrategy } - /// A constant value of `InvisibleElement` semantics with `subtreeStrategy: .ignore`. + /// A constant value of `InvisibleElement` semantics. static let constant = InvisibleElement() } +/// A semantics of an UI element that should be ignored when traversing view-tree. Unlike `InvisibleElement` this semantics cannot +/// be overwritten by any other. This means that next node recorders won't be asked for further check of a strictkier semantics. +internal struct IgnoredElement: NodeSemantics { + static var importance: Int = .max + let subtreeStrategy: NodeSubtreeStrategy + let nodes: [Node] = [] +} + /// A semantics of an UI element that is of `UIView` type. This semantics mean that the element has visual appearance in SR, but /// it will only utilize its base `UIView` attributes. The full identity of the node will remain ambiguous if not overwritten with `SpecificElement`. /// diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 532e9579b4..8baf944ea8 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -15,8 +15,10 @@ internal struct ViewTreeSnapshotBuilder { let viewTreeRecorder: ViewTreeRecorder /// Generates stable IDs for traversed views. let idsGenerator: NodeIDGenerator - /// Masks text in recorded nodes. - let textObfuscator: TextObfuscator + /// Text obfuscator applied to all non-sensitive texts. No-op if privacy mode is disabled. + let textObfuscator = TextObfuscator() + /// Text obfuscator applied to all sensitive texts. + let sensitiveTextObfuscator = SensitiveTextObfuscator() /// Builds the `ViewTreeSnapshot` for given root view. /// @@ -30,7 +32,19 @@ internal struct ViewTreeSnapshotBuilder { recorder: recorderContext, coordinateSpace: rootView, ids: idsGenerator, - textObfuscator: textObfuscator + textObfuscator: { + switch recorderContext.privacy { + case .maskAll: return textObfuscator + case .allowAll: return nopTextObfuscator + } + }(), + selectionTextObfuscator: { + switch recorderContext.privacy { + case .maskAll: return sensitiveTextObfuscator + case .allowAll: return nopTextObfuscator + } + }(), + sensitiveTextObfuscator: sensitiveTextObfuscator ) let snapshot = ViewTreeSnapshot( date: recorderContext.date.addingTimeInterval(recorderContext.rumContext.viewServerTimeOffset ?? 0), @@ -45,25 +59,36 @@ internal struct ViewTreeSnapshotBuilder { extension ViewTreeSnapshotBuilder { init() { self.init( - viewTreeRecorder: ViewTreeRecorder(nodeRecorders: defaultNodeRecorders), - idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + viewTreeRecorder: ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()), + idsGenerator: NodeIDGenerator() ) } } /// An arrays of default node recorders executed for the root view-tree hierarchy. -internal let defaultNodeRecorders: [NodeRecorder] = [ - UIViewRecorder(), - UILabelRecorder(), - UIImageViewRecorder(), - UITextFieldRecorder(), - UITextViewRecorder(), - UISwitchRecorder(), - UISliderRecorder(), - UISegmentRecorder(), - UIStepperRecorder(), - UINavigationBarRecorder(), - UITabBarRecorder(), - UIPickerViewRecorder(), -] +internal func createDefaultNodeRecorders() -> [NodeRecorder] { + let imageViewRecorder = UIImageViewRecorder() + imageViewRecorder.semanticsOverride = { imageView, _ in + let className = "\(type(of: imageView))" + // This gets effective on iOS 15.0+ which is the earliest version that displays some elements in popover views. + // Here we explicitly ignore the "shadow" effect applied to popover. + let isSystemShadow = className == "_UICutoutShadowView" + return isSystemShadow ? IgnoredElement(subtreeStrategy: .ignore) : nil + } + + return [ + UIViewRecorder(), + UILabelRecorder(), + imageViewRecorder, + UITextFieldRecorder(), + UITextViewRecorder(), + UISwitchRecorder(), + UISliderRecorder(), + UISegmentRecorder(), + UIStepperRecorder(), + UINavigationBarRecorder(), + UITabBarRecorder(), + UIPickerViewRecorder(), + UIDatePickerRecorder(), + ] +} diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index d8faefbe99..fe1931f8a1 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -267,6 +267,18 @@ extension SpecificElement { } } +internal class TextObfuscatorMock: TextObfuscating { + var result: (String) -> String = { $0 } + + func mask(text: String) -> String { + return result(text) + } +} + +internal func mockRandomTextObfuscator() -> TextObfuscating { + return [NOPTextObfuscator(), TextObfuscator(), SensitiveTextObfuscator()].randomElement()! +} + extension ViewTreeRecordingContext: AnyMockable, RandomMockable { public static func mockAny() -> ViewTreeRecordingContext { return .mockWith() @@ -277,7 +289,9 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { recorder: .mockRandom(), coordinateSpace: UIView.mockRandom(), ids: NodeIDGenerator(), - textObfuscator: TextObfuscator() + textObfuscator: mockRandomTextObfuscator(), + selectionTextObfuscator: mockRandomTextObfuscator(), + sensitiveTextObfuscator: mockRandomTextObfuscator() ) } @@ -285,13 +299,17 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { recorder: Recorder.Context = .mockAny(), coordinateSpace: UICoordinateSpace = UIView.mockAny(), ids: NodeIDGenerator = NodeIDGenerator(), - textObfuscator: TextObfuscator = TextObfuscator() + textObfuscator: TextObfuscating = NOPTextObfuscator(), + selectionTextObfuscator: TextObfuscating = NOPTextObfuscator(), + sensitiveTextObfuscator: TextObfuscating = NOPTextObfuscator() ) -> ViewTreeRecordingContext { return .init( recorder: recorder, coordinateSpace: coordinateSpace, ids: ids, - textObfuscator: textObfuscator + textObfuscator: textObfuscator, + selectionTextObfuscator: selectionTextObfuscator, + sensitiveTextObfuscator: sensitiveTextObfuscator ) } } diff --git a/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift b/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift index 1c02bca561..c512c6975f 100644 --- a/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Privacy/TextObfuscatorTests.swift @@ -51,7 +51,7 @@ class TestObfuscatorTests: XCTestCase { } class InputTextObfuscatorTests: XCTestCase { - let obfuscator = InputTextObfuscator() + let obfuscator = SensitiveTextObfuscator() func testWhenObfuscatingItAlwaysReplacesTextItWithConstantMask() { let expectedMask = "xxx" diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift new file mode 100644 index 0000000000..39460a8a98 --- /dev/null +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorderTests.swift @@ -0,0 +1,53 @@ +/* + * 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. + */ + +import XCTest +@testable import DatadogSessionReplay + +class UIDatePickerRecorderTests: XCTestCase { + private let recorder = UIDatePickerRecorder() + private let datePicker = UIDatePicker() + private var viewAttributes: ViewAttributes = .mockAny() + + func testWhenDatePickerIsNotVisible() throws { + // When + viewAttributes = .mock(fixture: .invisible) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: datePicker, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is InvisibleElement) + } + + func testWhenDatePickerIsVisibleAndHasSomeAppearance() throws { + // When + viewAttributes = .mock(fixture: .visible(.someAppearance)) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: datePicker, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is SpecificElement) + XCTAssertEqual(semantics.subtreeStrategy, .ignore) + XCTAssertTrue(semantics.nodes.first?.wireframesBuilder is UIDatePickerWireframesBuilder) + } + + func testWhenDatePickerIsVisibleAndHasNoAppearance() throws { + // When + viewAttributes = .mock(fixture: .visible(.noAppearance)) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: datePicker, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is SpecificElement) + XCTAssertEqual(semantics.subtreeStrategy, .ignore) + XCTAssertTrue(semantics.nodes.first?.wireframesBuilder is UIDatePickerWireframesBuilder) + } + + func testWhenViewIsNotOfExpectedType() { + // When + let view = UITextField() + + // Then + XCTAssertNil(recorder.semantics(of: view, with: viewAttributes, in: .mockAny())) + } +} diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift index 30fc3c36c0..515f530ec5 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorderTests.swift @@ -62,14 +62,20 @@ class UILabelRecorderTests: XCTestCase { label.text = .mockRandom() // When - let semantics1 = try XCTUnwrap(recorder.semantics(of: label, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .maskAll)))) - let semantics2 = try XCTUnwrap(recorder.semantics(of: label, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .allowAll)))) + let context: ViewTreeRecordingContext = .mockWith( + recorder: .mockWith(privacy: .mockRandom()), + textObfuscator: TextObfuscatorMock(), + selectionTextObfuscator: mockRandomTextObfuscator(), + sensitiveTextObfuscator: mockRandomTextObfuscator() + ) + let semantics = try XCTUnwrap(recorder.semantics(of: label, with: viewAttributes, in: context)) // Then - let builder1 = try XCTUnwrap(semantics1.nodes.first?.wireframesBuilder as? UILabelWireframesBuilder) - let builder2 = try XCTUnwrap(semantics2.nodes.first?.wireframesBuilder as? UILabelWireframesBuilder) - XCTAssertTrue(builder1.textObfuscator is TextObfuscator, "With `.maskAll` privacy the text obfuscator should be used") - XCTAssertTrue(builder2.textObfuscator is NOPTextObfuscator, "With `.allowAll` privacy the text obfuscator should not be used") + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UILabelWireframesBuilder) + XCTAssertTrue( + (builder.textObfuscator as? TextObfuscatorMock) === (context.textObfuscator as? TextObfuscatorMock), + "Labels should use default text obfuscator specific to current privacy mode" + ) } func testWhenViewIsNotOfExpectedType() { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift index 6ac980dcb2..c18746a1ee 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorderTests.swift @@ -72,20 +72,34 @@ class UITextFieldRecorderTests: XCTestCase { // When viewAttributes = .mock(fixture: .visible()) - let semantics1 = try XCTUnwrap(recorder.semantics(of: textField1, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .maskAll)))) - let semantics2 = try XCTUnwrap(recorder.semantics(of: textField1, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .allowAll)))) - let semantics3 = try XCTUnwrap(recorder.semantics(of: textField2, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .mockRandom())))) - let semantics4 = try XCTUnwrap(recorder.semantics(of: textField3, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .mockRandom())))) + let context: ViewTreeRecordingContext = .mockWith( + recorder: .mockWith(privacy: .mockRandom()), + textObfuscator: TextObfuscatorMock(), + selectionTextObfuscator: mockRandomTextObfuscator(), + sensitiveTextObfuscator: TextObfuscatorMock() + ) + + let semantics1 = try XCTUnwrap(recorder.semantics(of: textField1, with: viewAttributes, in: context)) + let semantics2 = try XCTUnwrap(recorder.semantics(of: textField2, with: viewAttributes, in: context)) + let semantics3 = try XCTUnwrap(recorder.semantics(of: textField3, with: viewAttributes, in: context)) // Then let builder1 = try XCTUnwrap(semantics1.nodes.first?.wireframesBuilder as? UITextFieldWireframesBuilder) let builder2 = try XCTUnwrap(semantics2.nodes.first?.wireframesBuilder as? UITextFieldWireframesBuilder) let builder3 = try XCTUnwrap(semantics3.nodes.first?.wireframesBuilder as? UITextFieldWireframesBuilder) - let builder4 = try XCTUnwrap(semantics4.nodes.first?.wireframesBuilder as? UITextFieldWireframesBuilder) - XCTAssertTrue(builder1.textObfuscator is TextObfuscator, "With `.maskAll` privacy the text obfuscator should be used") - XCTAssertTrue(builder2.textObfuscator is NOPTextObfuscator, "With `.allowAll` privacy the text obfuscator should not be used") - XCTAssertTrue(builder3.textObfuscator is InputTextObfuscator, "When `TextField` accepts secure text entry, it should use `InputTextObfuscator`") - XCTAssertTrue(builder4.textObfuscator is InputTextObfuscator, "When `TextField` accepts email or tlephone no. entry, it should use `InputTextObfuscator`") + + XCTAssertTrue( + (builder1.textObfuscator as? TextObfuscatorMock) === (context.textObfuscator as? TextObfuscatorMock), + "Non-sensitive text fields should use default text obfuscator specific to current privacy mode" + ) + XCTAssertTrue( + (builder2.textObfuscator as? TextObfuscatorMock) === (context.sensitiveTextObfuscator as? TextObfuscatorMock), + "Sensitive text fields should use sensitive text obfuscator no matter of privacy mode" + ) + XCTAssertTrue( + (builder3.textObfuscator as? TextObfuscatorMock) === (context.sensitiveTextObfuscator as? TextObfuscatorMock), + "Sensitive text fields should use sensitive text obfuscator no matter of privacy mode" + ) } func testWhenViewIsNotOfExpectedType() { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift index 160c00e1d7..781ecdffdd 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorderTests.swift @@ -54,14 +54,20 @@ class UITextViewRecorderTests: XCTestCase { // When viewAttributes = .mock(fixture: .visible()) - let semantics1 = try XCTUnwrap(recorder.semantics(of: textView, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .maskAll)))) - let semantics2 = try XCTUnwrap(recorder.semantics(of: textView, with: viewAttributes, in: .mockWith(recorder: .mockWith(privacy: .allowAll)))) + let context: ViewTreeRecordingContext = .mockWith( + recorder: .mockWith(privacy: .mockRandom()), + textObfuscator: TextObfuscatorMock(), + selectionTextObfuscator: mockRandomTextObfuscator(), + sensitiveTextObfuscator: mockRandomTextObfuscator() + ) + let semantics = try XCTUnwrap(recorder.semantics(of: textView, with: viewAttributes, in: context)) // Then - let builder1 = try XCTUnwrap(semantics1.nodes.first?.wireframesBuilder as? UITextViewWireframesBuilder) - let builder2 = try XCTUnwrap(semantics2.nodes.first?.wireframesBuilder as? UITextViewWireframesBuilder) - XCTAssertTrue(builder1.textObfuscator is TextObfuscator, "With `.maskAll` privacy the text obfuscator should be used") - XCTAssertTrue(builder2.textObfuscator is NOPTextObfuscator, "With `.allowAll` privacy the text obfuscator should not be used") + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UITextViewWireframesBuilder) + XCTAssertTrue( + (builder.textObfuscator as? TextObfuscatorMock) === (context.textObfuscator as? TextObfuscatorMock), + "Text views should use default text obfuscator specific to current privacy mode" + ) } func testWhenViewIsNotOfExpectedType() { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index 1cf142f58c..83f3d8cc81 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -147,7 +147,7 @@ class ViewTreeRecorderTests: XCTestCase { func testItRecordsInvisibleViews() { // Given - let recorder = ViewTreeRecorder(nodeRecorders: defaultNodeRecorders) + let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) let views: [UIView] = [ UIView.mock(withFixture: .invisible), UILabel.mock(withFixture: .invisible), @@ -167,7 +167,7 @@ class ViewTreeRecorderTests: XCTestCase { func testItRecordsViewsWithNoAppearance() { // Given - let recorder = ViewTreeRecorder(nodeRecorders: defaultNodeRecorders) + let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) let view = UIView.mock(withFixture: .visible(.noAppearance)) let label = UILabel.mock(withFixture: .visible(.noAppearance)) @@ -197,7 +197,7 @@ class ViewTreeRecorderTests: XCTestCase { func testItRecordsViewsWithSomeAppearance() { // Given - let recorder = ViewTreeRecorder(nodeRecorders: defaultNodeRecorders) + let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) let views: [UIView] = [ UIView.mock(withFixture: .visible(.someAppearance)), UILabel.mock(withFixture: .visible(.someAppearance)), diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift index 989fe9ab02..176c544e5b 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift @@ -12,20 +12,17 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { func testWhenQueryingNodeRecorders_itPassesAppropriateContext() throws { // Given let view = UIView(frame: .mockRandom()) - - let randomRecorderContext: Recorder.Context = .mockWith() + let randomRecorderContext: Recorder.Context = .mockRandom() let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), - idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + idsGenerator: NodeIDGenerator() ) // When let snapshot = builder.createSnapshot(of: view, with: randomRecorderContext) // Then - XCTAssertEqual(snapshot.date, randomRecorderContext.date) XCTAssertEqual(snapshot.rumContext, randomRecorderContext.rumContext) let queryContext = try XCTUnwrap(nodeRecorder.queryContexts.first) @@ -33,6 +30,32 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { XCTAssertEqual(queryContext.recorder, randomRecorderContext) } + func testItConfiguresTextObfuscatorsAccordinglyToCurrentPrivacyMode() throws { + // Given + let view = UIView(frame: .mockRandom()) + let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) + let builder = ViewTreeSnapshotBuilder( + viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), + idsGenerator: NodeIDGenerator() + ) + + // When + _ = builder.createSnapshot(of: view, with: .mockWith(privacy: .allowAll)) + _ = builder.createSnapshot(of: view, with: .mockWith(privacy: .maskAll)) + + // Then + let queriedContexts = nodeRecorder.queryContexts + XCTAssertEqual(queriedContexts.count, 2) + + XCTAssertTrue(queriedContexts[0].textObfuscator is NOPTextObfuscator) + XCTAssertTrue(queriedContexts[0].selectionTextObfuscator is NOPTextObfuscator) + XCTAssertTrue(queriedContexts[0].sensitiveTextObfuscator is SensitiveTextObfuscator) + + XCTAssertTrue(queriedContexts[1].textObfuscator is TextObfuscator) + XCTAssertTrue(queriedContexts[1].selectionTextObfuscator is SensitiveTextObfuscator) + XCTAssertTrue(queriedContexts[1].sensitiveTextObfuscator is SensitiveTextObfuscator) + } + func testItAppliesServerTimeOffsetToSnapshot() { // Given let now = Date() @@ -40,8 +63,7 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), - idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + idsGenerator: NodeIDGenerator() ) // When From 2878276ec99c7afa03e63156338022fa6a228761 Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Fri, 10 Mar 2023 15:59:36 -0500 Subject: [PATCH 15/72] RUMM-2872 Initial work for stopping sessions This includes most of the work for the SessionScope for stopping a session, but does not include any ApplicationScope work for restarting sessions or ViewScope work for sending session status to intake. --- Sources/Datadog/DDRUMMonitor.swift | 7 + .../Datadog/RUM/RUMContext/RUMContext.swift | 2 + .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 11 + .../Scopes/RUMApplicationScope.swift | 5 + .../RUMMonitor/Scopes/RUMSessionScope.swift | 67 ++++-- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 5 + Sources/Datadog/RUMMonitor.swift | 6 + .../Datadog/Mocks/RUMFeatureMocks.swift | 11 +- .../Scopes/RUMSessionScopeTests.swift | 210 +++++++++++++++++- .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 23 ++ 10 files changed, 316 insertions(+), 31 deletions(-) diff --git a/Sources/Datadog/DDRUMMonitor.swift b/Sources/Datadog/DDRUMMonitor.swift index 57400909e2..bb8cbf3b34 100644 --- a/Sources/Datadog/DDRUMMonitor.swift +++ b/Sources/Datadog/DDRUMMonitor.swift @@ -296,6 +296,13 @@ public class DDRUMMonitor { /// - Parameter key: key for the attribute that will be removed. public func removeAttribute(forKey key: AttributeKey) {} + // MARK: - Session + + /// Stops the current session. + /// A new session will start in response to a call to `startView` or `addUserAction`. + /// If the session is started because of a call to `addUserAction`, the last know view is restarted in the new session. + public func stopSession() {} + // MARK: - Internal internal init() {} diff --git a/Sources/Datadog/RUM/RUMContext/RUMContext.swift b/Sources/Datadog/RUM/RUMContext/RUMContext.swift index 5124697e4e..c813b28553 100644 --- a/Sources/Datadog/RUM/RUMContext/RUMContext.swift +++ b/Sources/Datadog/RUM/RUMContext/RUMContext.swift @@ -11,6 +11,8 @@ internal struct RUMContext { let rumApplicationID: String /// The ID of current RUM session. May change over time. var sessionID: RUMUUID + /// Whether the session for this context is currently active + var isSessionActive: Bool /// The ID of currently displayed view. var activeViewID: RUMUUID? diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index 404f7721bf..ee7d35b9b6 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -26,6 +26,17 @@ internal struct RUMApplicationStartCommand: RUMCommand { var isUserInteraction = false } +internal struct RUMStopSessionCommand: RUMCommand { + var time: Date + var attributes: [AttributeKey: AttributeValue] = [:] + var canStartBackgroundView = false // no, stopping a session should not start a backgorund session + var isUserInteraction = false + + init(time: Date) { + self.time = time + } +} + // MARK: - RUM View related commands internal struct RUMStartViewCommand: RUMCommand { diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 1652532fc7..d15c78e892 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -9,6 +9,10 @@ import Foundation internal class RUMApplicationScope: RUMScope, RUMContextProvider { // MARK: - Child Scopes + // Whether the applciation is already active. Set to true + // when the first session starts. + private(set) var appplicationActive: Bool = false + /// Session scope. It gets created with the first event. /// Might be re-created later according to session duration constraints. private(set) var sessionScope: RUMSessionScope? @@ -22,6 +26,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { self.context = RUMContext( rumApplicationID: dependencies.rumApplicationID, sessionID: .nullUUID, + isSessionActive: false, activeViewID: nil, activeViewPath: nil, activeViewName: nil, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 849505b05d..176c3cd52e 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -49,6 +49,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { let sessionUUID: RUMUUID /// If events from this session should be sampled (send to Datadog). let isSampled: Bool + /// If the session is currently active. Set to false on a StopSession command + var isActive: Bool /// If this is the very first session created in the current app process (`false` for session created upon expiration of a previous one). let isInitialSession: Bool /// The start time of this Session, measured in device date. In initial session this is the time of SDK init. @@ -71,6 +73,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { self.sessionStartTime = startTime self.lastInteractionTime = startTime self.backgroundEventTrackingEnabled = dependencies.backgroundEventTrackingEnabled + self.isActive = true self.state = RUMSessionState( sessionUUID: sessionUUID.rawValue, isInitialSession: isInitialSession, @@ -121,6 +124,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { var context: RUMContext { var context = parent.context context.sessionID = sessionUUID + context.isSessionActive = isActive return context } @@ -135,35 +139,45 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { } if !isSampled { - return true // discard all events in this session + // Make sure sessions end even if they are sampled + if command is RUMStopSessionCommand { + isActive = false + } + + return isActive // discard all events in this session } - if let startApplicationCommand = command as? RUMApplicationStartCommand { - startApplicationLaunchView(on: startApplicationCommand, context: context, writer: writer) - } else if let startViewCommand = command as? RUMStartViewCommand { - // Start view scope explicitly on receiving "start view" command - startView(on: startViewCommand, context: context) - } else if !hasActiveView { - // Otherwise, if there is no active view scope, consider starting artificial scope for handling this command - let handlingRule = RUMOffViewEventsHandlingRule( - sessionState: state, - isAppInForeground: context.applicationStateHistory.currentSnapshot.state.isRunningInForeground, - isBETEnabled: backgroundEventTrackingEnabled - ) + var deactivating = false + if isActive { + if command is RUMStopSessionCommand { + deactivating = true + } else if let startApplicationCommand = command as? RUMApplicationStartCommand { + startApplicationLaunchView(on: startApplicationCommand, context: context, writer: writer) + } else if let startViewCommand = command as? RUMStartViewCommand { + // Start view scope explicitly on receiving "start view" command + startView(on: startViewCommand, context: context) + } else if !hasActiveView { + // Otherwise, if there is no active view scope, consider starting artificial scope for handling this command + let handlingRule = RUMOffViewEventsHandlingRule( + sessionState: state, + isAppInForeground: context.applicationStateHistory.currentSnapshot.state.isRunningInForeground, + isBETEnabled: backgroundEventTrackingEnabled + ) - switch handlingRule { - case .handleInBackgroundView where command.canStartBackgroundView: - startBackgroundView(on: command, context: context) - default: - if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) - // As no view scope will handle this command, warn the user on dropping it. - DD.logger.warn( + switch handlingRule { + case .handleInBackgroundView where command.canStartBackgroundView: + startBackgroundView(on: command, context: context) + default: + if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) + // As no view scope will handle this command, warn the user on dropping it. + DD.logger.warn( """ \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using the RumMonitor.startView() and RumMonitor.stopView() methods. """ - ) + ) + } } } } @@ -171,15 +185,20 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Propagate command viewScopes = viewScopes.scopes(byPropagating: command, context: context, writer: writer) - if !hasActiveView { - // If there is no active view, update `CrashContext` accordingly, so eventual crash + if isActive && !hasActiveView { + // If this session is active and there is no active view, update `CrashContext` accordingly, so eventual crash // won't be associated to an inactive view and instead we will consider starting background view to track it. + // We also want to send this as a session is being stopped. // It means that with Background Events Tracking disabled, eventual off-view crashes will be dropped // similar to how we drop other events. dependencies.core.send(message: .custom(key: "rum", baggage: [RUMBaggageKeys.viewReset: true])) } - return true + if deactivating { + isActive = false + } + + return isActive || !viewScopes.isEmpty } /// If there is an active view. diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 00516452f5..e9d2597eaa 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -168,6 +168,11 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { didReceiveStartCommand = true needsViewUpdate = true + // Session stop + case let _ as RUMStopSessionCommand: + isActiveView = false + needsViewUpdate = true + // View commands case let command as RUMStartViewCommand where identity.equals(command.identity): if didReceiveStartCommand { diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index 81eaf1d094..262e964ac2 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -601,6 +601,12 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { } } + // MARK: - Session + + override public func stopSession() { + process(command: RUMStopSessionCommand(time: dateProvider.now)) + } + // MARK: - Internal func enableRUMDebugging(_ enabled: Bool) { diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 2f6218296e..b781746d76 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -149,7 +149,6 @@ struct RUMCommandMock: RUMCommand { var time = Date() var attributes: [AttributeKey: AttributeValue] = [:] var canStartBackgroundView = false - var canStartApplicationLaunchView = false var isUserInteraction = false } @@ -596,6 +595,14 @@ extension RUMAddFeatureFlagEvaluationCommand: AnyMockable, RandomMockable { } } +extension RUMStopSessionCommand: AnyMockable { + public static func mockAny() -> RUMStopSessionCommand { mockWith() } + + static func mockWith(time: Date = .mockAny()) -> RUMStopSessionCommand { + return RUMStopSessionCommand(time: time) + } +} + // MARK: - RUMCommand Property Mocks extension RUMInternalErrorSource: RandomMockable { @@ -620,6 +627,7 @@ extension RUMContext { static func mockWith( rumApplicationID: String = .mockAny(), sessionID: RUMUUID = .mockRandom(), + isSessionActive: Bool = true, activeViewID: RUMUUID? = nil, activeViewPath: String? = nil, activeViewName: String? = nil, @@ -628,6 +636,7 @@ extension RUMContext { return RUMContext( rumApplicationID: rumApplicationID, sessionID: sessionID, + isSessionActive: true, activeViewID: activeViewID, activeViewPath: activeViewPath, activeViewName: activeViewName, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 80aa641437..9c9459c74c 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -21,6 +21,7 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual(scope.context.rumApplicationID, "rum-123") XCTAssertNotEqual(scope.context.sessionID, .nullUUID) + XCTAssertTrue(scope.context.isSessionActive) XCTAssertNil(scope.context.activeViewID) XCTAssertNil(scope.context.activeViewPath) XCTAssertNil(scope.context.activeUserActionID) @@ -147,7 +148,7 @@ class RUMSessionScopeTests: XCTestCase { // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true, canStartApplicationLaunchView: .mockRandom()) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true) XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) // Then @@ -183,7 +184,7 @@ class RUMSessionScopeTests: XCTestCase { // When commandTime = commandTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true, canStartApplicationLaunchView: .mockRandom()) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true) XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) // Then @@ -212,7 +213,7 @@ class RUMSessionScopeTests: XCTestCase { // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: false, canStartApplicationLaunchView: .mockRandom()) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: false) XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) // Then @@ -238,7 +239,7 @@ class RUMSessionScopeTests: XCTestCase { // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: false) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom()) XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) // Then @@ -266,7 +267,7 @@ class RUMSessionScopeTests: XCTestCase { // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: true) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom()) XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) // Then @@ -409,6 +410,203 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertNil(viewEvent, "Crash context must not include rum view event, because there is no active view") } + // MARK: - Stopping Sessions + + func testGivenActiveSession_whenStopSessionEvent_itSetsSessionActiveFalse() { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + + // When + let command = RUMStopSessionCommand.mockWith(time: Date()) + + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertFalse(scope.isActive) + XCTAssertFalse(result) + } + + func testGivenStoppedSession_itUpdatesContext() { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // When + let context = scope.context + + XCTAssertFalse(context.isSessionActive) + } + + func testGivenActiveSessionWithActiveView_whenStopSessionEvent_itStopsTheActiveView() throws { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: Date()), context: context, writer: writer) + let view = try XCTUnwrap(scope.viewScopes.first) + + // When + let command = RUMStopSessionCommand.mockWith(time: Date()) + + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertFalse(view.isActiveView) + XCTAssertFalse(result) + } + + func testWhenSessionScopeHasViewsWithPendingResources_whenStopSetssion_itReturnsTrueFromProcess() throws { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: Date()), context: context, writer: writer) + _ = scope.process(command: RUMStartResourceCommand.mockWith(time: Date()), context: context, writer: writer) + let view = try XCTUnwrap(scope.viewScopes.first) + + // When + let command = RUMStopSessionCommand(time: Date()) + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertFalse(scope.isActive) + XCTAssertFalse(view.isActiveView) + // This still needs to return true because we have pending events + XCTAssertTrue(result) + } + + func testWhenSessionScopeHasViewsWithPendingResources_itReturnsTrueFromProcessWhenResourcesFinish() throws { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: Date()), context: context, writer: writer) + let startResourceCommand = RUMStartResourceCommand.mockWith(time: Date()) + _ = scope.process(command: startResourceCommand, context: context, writer: writer) + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // When + let command = RUMStopResourceCommand.mockWith(resourceKey: startResourceCommand.resourceKey, time: Date()) + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertFalse(scope.isActive) + XCTAssertFalse(result) + } + + func testWhenScopeEnded_itDoesNotStartNewViews() throws { + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // When + let command = RUMStartViewCommand.mockWith(time: Date()) + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertFalse(result) + } + + func testWhenScopeEnded_itDoesNotCreateAnApplicationLaunchView() { + // Note - This should happen because the application context should prevent against + // it, but just in case + // Given + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date() + ) + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // When + let command = RUMApplicationStartCommand(time: Date(), attributes: [:]) + let result = scope.process(command: command, context: context, writer: writer) + + // Then + XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertFalse(result) + } + + func testWhenScopeEnded_itUpdatesContext() { + // Given + var viewEvent: RUMViewEvent? = nil + let messageReciever = FeatureMessageReceiverMock { message in + if case let .custom(_, baggage) = message, let event = baggage[RUMBaggageKeys.viewEvent, type: RUMViewEvent.self] { + viewEvent = event + } else if case let .custom(_, baggage) = message, baggage[RUMBaggageKeys.viewReset, type: Bool.self] == true { + viewEvent = nil + } + } + + let core = PassthroughCoreMock( + context: context, + messageReceiver: messageReciever + ) + + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date(), + dependencies: .mockWith(core: core) + ) + + let command = RUMStartViewCommand.mockWith(time: Date(), identity: mockView) + _ = scope.process(command: command, context: context, writer: writer) + + // When + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + + // Then + XCTAssertNil(viewEvent) + } + + func testWhenScopeEnded_itDoesNotResetContextNextUpdate() { + // Given + var viewResetCallCount = 0 + var viewEvent: RUMViewEvent? = nil + let messageReciever = FeatureMessageReceiverMock { message in + if case let .custom(_, baggage) = message, baggage[RUMBaggageKeys.viewReset, type: Bool.self] == true { + viewResetCallCount += 1 + viewEvent = nil + } + } + + let core = PassthroughCoreMock( + context: context, + messageReceiver: messageReciever + ) + + let scope: RUMSessionScope = .mockWith( + parent: parent, + startTime: Date(), + dependencies: .mockWith(core: core) + ) + + let startViewCommand = RUMStartViewCommand.mockWith(time: Date(), identity: mockView) + _ = scope.process(command: startViewCommand, context: context, writer: writer) + let startResourceCommand = RUMStartResourceCommand.mockWith(time: Date()) + _ = scope.process(command: startResourceCommand, context: context, writer: writer) + + // When + _ = scope.process(command: RUMStopSessionCommand.mockWith(time: Date()), context: context, writer: writer) + let stopResourceCommand = RUMStopResourceCommand.mockWith(resourceKey: startResourceCommand.resourceKey, time: Date()) + _ = scope.process(command: stopResourceCommand, context: context, writer: writer) + + // Then + XCTAssertEqual(viewResetCallCount, 1) + } + // MARK: - Usage func testGivenSessionWithNoActiveScope_whenReceivingRUMCommandOtherThanKeepSessionAliveCommand_itLogsWarning() throws { @@ -431,7 +629,7 @@ class RUMSessionScopeTests: XCTestCase { return dd.logger.warnLog?.message } - let randomCommand = RUMCommandMock(time: Date(), canStartBackgroundView: false, canStartApplicationLaunchView: false) + let randomCommand = RUMCommandMock(time: Date(), canStartBackgroundView: false) let randomCommandLog = try XCTUnwrap(recordWarningOnReceiving(command: randomCommand)) XCTAssertEqual( randomCommandLog, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index 018ea777b4..c3f588e359 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -1606,6 +1606,29 @@ class RUMViewScopeTests: XCTestCase { ) } + // MARK: - Stopped Session + + func testGivenSession_whenSessionStopped_itSendsViewUpdateWithStopped() { + let initialDeviceTime: Date = .mockDecember15th2019At10AMUTC() + let initialServerTimeOffset: TimeInterval = 120 // 2 minutes + var currentDeviceTime = initialDeviceTime + + + // Given + let scope = RUMViewScope( + isInitialView: false, + parent: parent, + dependencies: .mockAny(), + identity: mockView, + path: .mockAny(), + name: .mockAny(), + attributes: [:], + customTimings: [:], + startTime: initialDeviceTime, + serverTimeOffset: initialServerTimeOffset + ) + } + // MARK: - Dates Correction func testGivenViewStartedWithServerTimeDifference_whenDifferentEventsAreSend_itAppliesTheSameCorrectionToAll() throws { From c4c76fd630c4b2d07d2a7ed19b70a77fc160e201 Mon Sep 17 00:00:00 2001 From: Rosa Trieu Date: Fri, 10 Mar 2023 17:33:17 -0800 Subject: [PATCH 16/72] Replaces Mobile Vitals image, updates text and links --- docs/rum_mobile_vitals.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/rum_mobile_vitals.md b/docs/rum_mobile_vitals.md index d2eb93ed15..c5f2777b3f 100644 --- a/docs/rum_mobile_vitals.md +++ b/docs/rum_mobile_vitals.md @@ -2,9 +2,9 @@ Real User Monitoring offers Mobile Vitals, a set of metrics inspired by [MetricKit][1], that can help compute insights about your mobile application's responsiveness, stability, and resource consumption. Mobile Vitals range from poor, moderate, to good. -Mobile Vitals appear in your application's **Overview** tab and in the side panel under **Performance** > **Event Timings and Mobile Vitals** when you click on an individual view in the [RUM Explorer][2]. Click on a graph in **Mobile Vitals** to apply a filter by version or examine filtered sessions. +Mobile Vitals appear on your your application's **Performance Overview** page when you navigate to **UX Monitoring > Performance Monitoring** and click your application. From the mobile performance dashboard for your application, click on a graph in **Mobile Vitals** to apply a filter by version or examine filtered sessions. -{{< img src="real_user_monitoring/ios/ios_mobile_vitals.png" alt="Mobile Vitals in the Performance Tab" style="width:70%;">}} +{{< img src="real_user_monitoring/ios/ios-mobile-vitals-new.png" alt="Mobile Vitals in the Performance Tab" style="width:70%;">}} Understand your application's overall health and performance with the line graphs displaying metrics across various application versions. To filter on application version or see specific sessions and views, click on a graph. @@ -17,23 +17,21 @@ You can also select a view in the RUM Explorer and observe recommended benchmark The following metrics provide insight into your mobile application's performance. | Measurement | Description | |--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Slow renders | To ensure a smooth, [jank-free][3] user experience, your application should render frames in under 60Hz.

RUM tracks the application’s [display refresh rate][4] using `@view.refresh_rate_average` and `@view.refresh_rate_min` view attributes.

With slow rendering, you can monitor which views are taking longer than 16ms or 60Hz to render.
**Note:** Refresh rates are normalized on a range of zero to 60fps. For example, if your application runs at 100fps on a device capable of rendering 120fps, Datadog reports 50fps in **Mobile Vitals**. | -| Frozen frames | Frames that take longer than 700ms to render appear as stuck and unresponsive in your application. These are classified as [frozen frames][5].

RUM tracks `long task` events with the duration for any task taking longer then 100ms to complete.

With frozen frames, you can monitor which views appear frozen (taking longer than 700ms to render) to your end users and eliminate jank in your application. | -| Crash-free sessions by version | An [application crash][7] is reported due to an unexpected exit in the application typically caused by an unhandled exception or signal. Crash-free user sessions in your application directly correspond to your end user’s experience and overall satisfaction.

RUM tracks complete crash reports and presents trends over time with [Error Tracking][8].

With crash-free sessions, you can stay up to speed on industry benchmarks and ensure that your application is ranked highly on the Google Play Store. | -| CPU ticks per second | High CPU usage impacts the [battery life][9] on your users’ devices.

RUM tracks CPU ticks per second for each view and the CPU utilization over the course of a session. The recommended range is <40 for good and <60 for moderate.

You can see the top views with the most number of CPU ticks on average over a selected time period under **Mobile Vitals** in your application's Overview page. | -| Memory utilization | High memory usage can lead to [out-of-memory crashes][10], which causes a poor user experience.

RUM tracks the amount of physical memory used by your application in bytes for each view, over the course of a session. The recommended range is <200MB for good and <400MB for moderate.

You can see the top views with the most memory consumption on average over a selected time period under **Mobile Vitals** in your application's Overview page. | +| Slow renders | To ensure a smooth, [jank-free][2] user experience, your application should render frames in under 60Hz.

RUM tracks the application’s [display refresh rate][3] using `@view.refresh_rate_average` and `@view.refresh_rate_min` view attributes.

With slow rendering, you can monitor which views are taking longer than 16ms or 60Hz to render.
**Note:** Refresh rates are normalized on a range of zero to 60fps. For example, if your application runs at 100fps on a device capable of rendering 120fps, Datadog reports 50fps in **Mobile Vitals**. | +| Frozen frames | Frames that take longer than 700ms to render appear as stuck and unresponsive in your application. These are classified as [frozen frames][4].

RUM tracks `long task` events with the duration for any task taking longer then 100ms to complete.

With frozen frames, you can monitor which views appear frozen (taking longer than 700ms to render) to your end users and eliminate jank in your application. | +| Crash-free sessions by version | An [application crash][5] is reported due to an unexpected exit in the application typically caused by an unhandled exception or signal. Crash-free user sessions in your application directly correspond to your end user’s experience and overall satisfaction.

RUM tracks complete crash reports and presents trends over time with [Error Tracking][6].

With crash-free sessions, you can stay up to speed on industry benchmarks and ensure that your application is ranked highly on the Google Play Store. | +| CPU ticks per second | High CPU usage impacts the [battery life][7] on your users’ devices.

RUM tracks CPU ticks per second for each view and the CPU utilization over the course of a session. The recommended range is <40 for good and <60 for moderate.

You can see the top views with the most number of CPU ticks on average over a selected time period under **Mobile Vitals** in your application's Overview page. | +| Memory utilization | High memory usage can lead to [out-of-memory crashes][8], which causes a poor user experience.

RUM tracks the amount of physical memory used by your application in bytes for each view, over the course of a session. The recommended range is <200MB for good and <400MB for moderate.

You can see the top views with the most memory consumption on average over a selected time period under **Mobile Vitals** in your application's Overview page. | ## Further Reading {{< partial name="whats-next/whats-next.html" >}} [1]: https://developer.apple.com/documentation/metrickit -[2]: https://app.datadoghq.com/rum/explorer -[3]: https://developer.android.com/topic/performance/vitals/render#common-jank -[4]: https://developer.android.com/guide/topics/media/frame-rate -[5]: https://developer.android.com/topic/performance/vitals/frozen -[6]: https://developer.android.com/topic/performance/vitals/anr -[7]: https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs -[8]: https://docs.datadoghq.com/real_user_monitoring/ios/crash_reporting/ -[9]: https://developer.apple.com/documentation/xcode/analyzing-your-app-s-battery-use/ -[10]: https://developer.android.com/reference/java/lang/OutOfMemoryError \ No newline at end of file +[2]: https://developer.android.com/topic/performance/vitals/render#common-jank +[3]: https://developer.android.com/guide/topics/media/frame-rate +[4]: https://developer.android.com/topic/performance/vitals/frozen +[5]: https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs +[6]: https://docs.datadoghq.com/real_user_monitoring/ios/crash_reporting/ +[7]: https://developer.apple.com/documentation/xcode/analyzing-your-app-s-battery-use/ +[8]: https://developer.android.com/reference/java/lang/OutOfMemoryError \ No newline at end of file From 86e76e9cc742cdcf7eb93d25a94e9d89229b20d4 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 8 Mar 2023 13:44:54 +0000 Subject: [PATCH 17/72] REPLAY-1347 Proper image classification --- .../Utilities/ImageDataProvider.swift | 63 ++++++++++++------- .../NodeRecorders/UIImageViewRecorder.swift | 22 +++++-- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 1d8c5511a6..c30ae87a40 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -8,19 +8,16 @@ import Foundation import UIKit internal class ImageDataProvider { - enum DataLoadingStatus: Encodable { - case loaded(_ base64: String), ignored - } - private var cache: Cache + private var cache: Cache private let maxBytesSize: Int private let maxDimensions: CGSize internal init( - cache: Cache = .init(), - maxBytesSize: Int = 64_000, - maxDimensions: CGSize = CGSize(width: 120, height: 120) + cache: Cache = .init(), + maxBytesSize: Int = 10_000, + maxDimensions: CGSize = CGSize(width: 40, height: 40) ) { self.cache = cache self.maxBytesSize = maxBytesSize @@ -30,7 +27,7 @@ internal class ImageDataProvider { func contentBase64String( of image: UIImage?, tintColor: UIColor? = nil - ) -> String? { + ) -> String { autoreleasepool { guard var image = image else { return "" @@ -43,20 +40,12 @@ internal class ImageDataProvider { if let tintColorIdentifier = tintColor?.srIdentifier { identifier += tintColorIdentifier } - let dataLoadingStaus = cache[identifier] - switch dataLoadingStaus { - case .none: - if let imageData = image.pngData(), image.size <= maxDimensions && imageData.count <= maxBytesSize { - let base64EncodedImage = imageData.base64EncodedString() - cache[identifier, base64EncodedImage.count] = .loaded(base64EncodedImage) - } else { - cache[identifier] = .ignored - } - return contentBase64String(of: image) - case .loaded(let base64String): - return base64String - case .ignored: - return "" + if let base64EncodedImage = cache[identifier] { + return base64EncodedImage + } else { + let base64EncodedImage = image.compressToTargetSize(maxBytesSize).base64EncodedString() + cache[identifier, base64EncodedImage.count] = base64EncodedImage + return base64EncodedImage } } } @@ -74,6 +63,36 @@ extension UIImage { } } +fileprivate extension UIImage { + func compressToTargetSize(_ targetSize: Int) -> Data { + var compressionQuality: CGFloat = 1.0 + guard var imageData = pngData() else { + return Data() + } + guard imageData.count >= targetSize else { + return imageData + } + var image = self + while imageData.count > targetSize { + compressionQuality -= 0.1 + imageData = image.jpegData(compressionQuality: compressionQuality) ?? Data() + + if imageData.count > targetSize { + image = image.scaledImage(by: 0.9) + } + } + return imageData + } + + func scaledImage(by percentage: CGFloat) -> UIImage { + let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) + let renderer = UIGraphicsImageRenderer(size: newSize) + return renderer.image { context in + draw(in: CGRect(origin: .zero, size: newSize)) + } + } +} + extension UIColor { var srIdentifier: String { return "\(hash)" diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 5e9fa9b6f6..cd995dd4b2 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,6 +7,7 @@ import UIKit internal struct UIImageViewRecorder: NodeRecorder { + private let imageDataProvider = ImageDataProvider() func semantics( @@ -31,6 +32,7 @@ internal struct UIImageViewRecorder: NodeRecorder { } else { contentFrame = nil } + let builder = UIImageViewWireframesBuilder( wireframeID: ids[0], imageWireframeID: ids[1], @@ -38,7 +40,7 @@ internal struct UIImageViewRecorder: NodeRecorder { contentFrame: contentFrame, clipsToBounds: imageView.clipsToBounds, image: imageView.image, - imageTintColor: imageView.tintColor, + imageMainThreadDescription: imageView.image?.description, imageDataProvider: imageDataProvider ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) @@ -63,7 +65,7 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { let image: UIImage? - let imageTintColor: UIColor? + let imageMainThreadDescription: String? let imageDataProvider: ImageDataProvider @@ -102,13 +104,15 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { opacity: attributes.alpha ) ] + var base64: String = "" + if #available(iOS 13.0, *), image?.isSymbolImage == true || imageMainThreadDescription?.isBundled == true { + base64 = imageDataProvider.contentBase64String(of: image) + } + if let contentFrame = contentFrame { wireframes.append( builder.createImageWireframe( - base64: imageDataProvider.contentBase64String( - of: image, - tintColor: imageTintColor - ), + base64: base64, id: imageWireframeID, frame: contentFrame, clip: clipsToBounds ? clip : nil @@ -118,3 +122,9 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { return wireframes } } + +fileprivate extension String { + var isBundled: Bool { + return contains("named(") + } +} From 341ea8f8c49dd6e27b46d493515493f8c5bbe9df Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 8 Mar 2023 16:23:03 +0000 Subject: [PATCH 18/72] REPLAY-1347 Fix unit tests --- .../Utilities/ImageDataProvider.swift | 5 +-- .../NodeRecorders/UIImageViewRecorder.swift | 40 +++++++++++++++---- .../Utilties/ImageDataProviderTests.swift | 20 ---------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index c30ae87a40..0552f0f8b3 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -12,16 +12,13 @@ internal class ImageDataProvider { private var cache: Cache private let maxBytesSize: Int - private let maxDimensions: CGSize internal init( cache: Cache = .init(), - maxBytesSize: Int = 10_000, - maxDimensions: CGSize = CGSize(width: 40, height: 40) + maxBytesSize: Int = 10_000 ) { self.cache = cache self.maxBytesSize = maxBytesSize - self.maxDimensions = maxDimensions } func contentBase64String( diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index cd995dd4b2..b2b2baf43f 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,8 +7,27 @@ import UIKit internal struct UIImageViewRecorder: NodeRecorder { + private let imageDataProvider: ImageDataProvider + private let tintColorProvider: (UIImageView) -> UIColor? + private let shouldRecordImagePredicate: (UIImageView) -> Bool - private let imageDataProvider = ImageDataProvider() + internal init( + imageDataProvider: ImageDataProvider = ImageDataProvider(), + tintColorProvider: @escaping (UIImageView) -> UIColor? = { _ in + return nil + }, + shouldRecordImagePredicate: @escaping (UIImageView) -> Bool = { imageView in + if #available(iOS 13.0, *) { + return imageView.image?.isSymbolImage == true || imageView.image?.description.isBundled == true + } else { + return false + } + } + ) { + self.imageDataProvider = imageDataProvider + self.tintColorProvider = tintColorProvider + self.shouldRecordImagePredicate = shouldRecordImagePredicate + } func semantics( of view: UIView, @@ -32,7 +51,6 @@ internal struct UIImageViewRecorder: NodeRecorder { } else { contentFrame = nil } - let builder = UIImageViewWireframesBuilder( wireframeID: ids[0], imageWireframeID: ids[1], @@ -40,8 +58,9 @@ internal struct UIImageViewRecorder: NodeRecorder { contentFrame: contentFrame, clipsToBounds: imageView.clipsToBounds, image: imageView.image, - imageMainThreadDescription: imageView.image?.description, - imageDataProvider: imageDataProvider + imageDataProvider: imageDataProvider, + tintColor: tintColorProvider(imageView), + shouldRecordImage: shouldRecordImagePredicate(imageView) ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return SpecificElement(subtreeStrategy: .record, nodes: [node]) @@ -65,10 +84,12 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { let image: UIImage? - let imageMainThreadDescription: String? - let imageDataProvider: ImageDataProvider + let tintColor: UIColor? + + let shouldRecordImage: Bool + private var clip: SRContentClip? { guard let contentFrame = contentFrame else { return nil @@ -105,8 +126,11 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { ) ] var base64: String = "" - if #available(iOS 13.0, *), image?.isSymbolImage == true || imageMainThreadDescription?.isBundled == true { - base64 = imageDataProvider.contentBase64String(of: image) + if shouldRecordImage { + base64 = imageDataProvider.contentBase64String( + of: image, + tintColor: tintColor + ) } if let contentFrame = contentFrame { diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift index 50ff5577f7..d7f5d241bc 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift @@ -41,26 +41,6 @@ class ImageDataProviderTests: XCTestCase { XCTAssertGreaterThan(imageData.count, 0) } - func test_ignoresAboveSize() throws { - let sut = ImageDataProvider( - maxBytesSize: 1 - ) - let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil) - - let imageData = try XCTUnwrap(sut.contentBase64String(of: image)) - XCTAssertEqual(imageData.count, 0) - } - - func test_ignoresAboveDimensions() throws { - let sut = ImageDataProvider( - maxDimensions: CGSize(width: 1, height: 1) - ) - let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil) - - let imageData = try XCTUnwrap(sut.contentBase64String(of: image)) - XCTAssertEqual(imageData.count, 0) - } - func test_imageIdentifierConsistency() { var ids = Set() for _ in 0..<100 { From ddb34688404a9fab3a08be73f0f70d4ab8d5fbd8 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 10 Mar 2023 11:34:48 +0000 Subject: [PATCH 19/72] REPLAY-1347 Remove scaling --- .../Utilities/ImageDataProvider.swift | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 0552f0f8b3..8c7fd10d1d 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -8,7 +8,6 @@ import Foundation import UIKit internal class ImageDataProvider { - private var cache: Cache private let maxBytesSize: Int @@ -40,7 +39,7 @@ internal class ImageDataProvider { if let base64EncodedImage = cache[identifier] { return base64EncodedImage } else { - let base64EncodedImage = image.compressToTargetSize(maxBytesSize).base64EncodedString() + let base64EncodedImage = image.pngData()?.base64EncodedString() ?? "" cache[identifier, base64EncodedImage.count] = base64EncodedImage return base64EncodedImage } @@ -60,36 +59,6 @@ extension UIImage { } } -fileprivate extension UIImage { - func compressToTargetSize(_ targetSize: Int) -> Data { - var compressionQuality: CGFloat = 1.0 - guard var imageData = pngData() else { - return Data() - } - guard imageData.count >= targetSize else { - return imageData - } - var image = self - while imageData.count > targetSize { - compressionQuality -= 0.1 - imageData = image.jpegData(compressionQuality: compressionQuality) ?? Data() - - if imageData.count > targetSize { - image = image.scaledImage(by: 0.9) - } - } - return imageData - } - - func scaledImage(by percentage: CGFloat) -> UIImage { - let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) - let renderer = UIGraphicsImageRenderer(size: newSize) - return renderer.image { context in - draw(in: CGRect(origin: .zero, size: newSize)) - } - } -} - extension UIColor { var srIdentifier: String { return "\(hash)" From 2b38dbcaf0bb2a8ac613dd7bbc1be3c23126ed12 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 10 Mar 2023 13:33:16 +0000 Subject: [PATCH 20/72] REPLAY-1347 Add tests --- .../UIImageViewRecorderTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift index 82a0d7623c..ee25e5f199 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift @@ -51,6 +51,34 @@ class UIImageViewRecorderTests: XCTestCase { XCTAssertTrue(semantics.nodes.first?.wireframesBuilder is UIImageViewWireframesBuilder) } + func testWhenShouldRecordImagePredicateReturnsFalse() throws { + // When + let recorder = UIImageViewRecorder(shouldRecordImagePredicate: { _ in return false }) + imageView.image = UIImage() + viewAttributes = .mock(fixture: .visible()) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: imageView, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is SpecificElement) + XCTAssertEqual(semantics.subtreeStrategy, .record, "Image view's subtree should be recorded") + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIImageViewWireframesBuilder) + XCTAssertFalse(builder.shouldRecordImage) + } + + func testWhenTintColorIsProvider() throws { + // When + let recorder = UIImageViewRecorder(tintColorProvider: { _ in return .red }) + imageView.image = UIImage() + viewAttributes = .mock(fixture: .visible()) + + // Then + let semantics = try XCTUnwrap(recorder.semantics(of: imageView, with: viewAttributes, in: .mockAny())) + XCTAssertTrue(semantics is SpecificElement) + XCTAssertEqual(semantics.subtreeStrategy, .record, "Image view's subtree should be recorded") + let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIImageViewWireframesBuilder) + XCTAssertEqual(builder.tintColor, .red) + } + func testWhenViewIsNotOfExpectedType() { // When let view = UITextField() From 1c4b5efa46d8fae1f24e29198f42c3fa66eed73f Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 10 Mar 2023 14:00:02 +0000 Subject: [PATCH 21/72] REPLAY-1347 Add image wireframes builder tests --- .../Utilities/ImageDataProvider.swift | 19 +++- .../NodeRecorders/UIImageViewRecorder.swift | 2 +- .../UIImageViewWireframesBuilderTests.swift | 94 +++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 8c7fd10d1d..3f813b4593 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -7,7 +7,18 @@ import Foundation import UIKit -internal class ImageDataProvider { +internal protocol ImageDataProviding { + func contentBase64String( + of image: UIImage? + ) -> String + + func contentBase64String( + of image: UIImage?, + tintColor: UIColor? + ) -> String +} + +internal class ImageDataProvider: ImageDataProviding { private var cache: Cache private let maxBytesSize: Int @@ -22,7 +33,7 @@ internal class ImageDataProvider { func contentBase64String( of image: UIImage?, - tintColor: UIColor? = nil + tintColor: UIColor? ) -> String { autoreleasepool { guard var image = image else { @@ -45,6 +56,10 @@ internal class ImageDataProvider { } } } + + func contentBase64String(of image: UIImage?) -> String { + contentBase64String(of: image, tintColor: nil) + } } fileprivate extension CGSize { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index b2b2baf43f..83815616b7 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -84,7 +84,7 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { let image: UIImage? - let imageDataProvider: ImageDataProvider + let imageDataProvider: ImageDataProviding let tintColor: UIColor? diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift new file mode 100644 index 0000000000..5bfc56e8b5 --- /dev/null +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -0,0 +1,94 @@ +/* + * 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. + */ + +import XCTest +@testable import DatadogSessionReplay + +class UIImageViewWireframesBuilderTests: XCTestCase { + var wireframesBuilder: WireframesBuilder! + + override func setUp() { + super.setUp() + wireframesBuilder = WireframesBuilder() + } + + override func tearDown() { + wireframesBuilder = nil + super.tearDown() + } + + func test_BuildCorrectWireframes_fromValidData() { + let wireframeID = WireframeID.mockRandom() + let imageWireframeID = WireframeID.mockRandom() + let builder = UIImageViewWireframesBuilder( + wireframeID: wireframeID, + imageWireframeID: imageWireframeID, + attributes: ViewAttributes.mock(fixture: .visible(.someAppearance)), + contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), + clipsToBounds: true, + image: UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil), + imageDataProvider: MockImageDataProvider(), + tintColor: UIColor.mockRandom(), + shouldRecordImage: true + ) + + let wireframes = builder.buildWireframes(with: wireframesBuilder) + + XCTAssertEqual(wireframes.count, 2) + + if case let .shapeWireframe(shapeWireframe) = wireframes[0] { + XCTAssertEqual(shapeWireframe.id, wireframeID) + } else { + XCTFail("First wireframe needs to be shapeWireframe case") + } + + if case let .imageWireframe(imageWireframe) = wireframes[1] { + XCTAssertEqual(imageWireframe.id, imageWireframeID) + XCTAssertEqual(imageWireframe.base64, "mock_base64_string") + } else { + XCTFail("Second wireframe needs to be imageWireframe case") + } + } + + func test_BuildCorrectWireframes_whenContentImageIsIgnored() { + let wireframeID = WireframeID.mockRandom() + let imageWireframeID = WireframeID.mockRandom() + let builder = UIImageViewWireframesBuilder( + wireframeID: wireframeID, + imageWireframeID: imageWireframeID, + attributes: ViewAttributes.mock(fixture: .visible(.someAppearance)), + contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), + clipsToBounds: true, + image: UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil), + imageDataProvider: MockImageDataProvider(), + tintColor: UIColor.mockRandom(), + shouldRecordImage: false + ) + + let wireframes = builder.buildWireframes(with: wireframesBuilder) + + XCTAssertEqual(wireframes.count, 2) + + if case let .shapeWireframe(shapeWireframe) = wireframes[0] { + XCTAssertEqual(shapeWireframe.id, wireframeID) + } else { + XCTFail("First wireframe needs to be shapeWireframe case") + } + + if case let .imageWireframe(imageWireframe) = wireframes[1] { + XCTAssertEqual(imageWireframe.id, imageWireframeID) + XCTAssertEqual(imageWireframe.base64, "") + } else { + XCTFail("Second wireframe needs to be imageWireframe case") + } + } +} + +class MockImageDataProvider: ImageDataProvider { + override func contentBase64String(of image: UIImage?, tintColor: UIColor?) -> String { + return "mock_base64_string" + } +} From f3cdddf9ff5b0e0b955d83a2d953dbb693bdba27 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 10 Mar 2023 14:07:17 +0000 Subject: [PATCH 22/72] REPLAY-1347 Move dependency to the context --- .../NodeRecorders/UIImageViewRecorder.swift | 5 +---- .../ViewTreeSnapshot/ViewTreeRecorder.swift | 2 ++ .../ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift | 8 ++++++-- DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift | 9 ++++++--- .../ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift | 6 ++++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 83815616b7..b21c1c8cb0 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,12 +7,10 @@ import UIKit internal struct UIImageViewRecorder: NodeRecorder { - private let imageDataProvider: ImageDataProvider private let tintColorProvider: (UIImageView) -> UIColor? private let shouldRecordImagePredicate: (UIImageView) -> Bool internal init( - imageDataProvider: ImageDataProvider = ImageDataProvider(), tintColorProvider: @escaping (UIImageView) -> UIColor? = { _ in return nil }, @@ -24,7 +22,6 @@ internal struct UIImageViewRecorder: NodeRecorder { } } ) { - self.imageDataProvider = imageDataProvider self.tintColorProvider = tintColorProvider self.shouldRecordImagePredicate = shouldRecordImagePredicate } @@ -58,7 +55,7 @@ internal struct UIImageViewRecorder: NodeRecorder { contentFrame: contentFrame, clipsToBounds: imageView.clipsToBounds, image: imageView.image, - imageDataProvider: imageDataProvider, + imageDataProvider: context.imageDataProvider, tintColor: tintColorProvider(imageView), shouldRecordImage: shouldRecordImagePredicate(imageView) ) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift index c9f2101fa3..10502fc773 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift @@ -19,6 +19,8 @@ internal struct ViewTreeRecordingContext { /// Masks text in recorded nodes. /// Can be overwriten in by `NodeRecorder` if their subtree recording requires different masking. var textObfuscator: TextObfuscating + /// Provides base64 image data with a built in caching mechanism. + let imageDataProvider: ImageDataProviding } internal struct ViewTreeRecorder { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 532e9579b4..0e0628f7b4 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -17,6 +17,8 @@ internal struct ViewTreeSnapshotBuilder { let idsGenerator: NodeIDGenerator /// Masks text in recorded nodes. let textObfuscator: TextObfuscator + /// Provides base64 image data with a built in caching mechanism. + let imageDataProvider: ImageDataProvider /// Builds the `ViewTreeSnapshot` for given root view. /// @@ -30,7 +32,8 @@ internal struct ViewTreeSnapshotBuilder { recorder: recorderContext, coordinateSpace: rootView, ids: idsGenerator, - textObfuscator: textObfuscator + textObfuscator: textObfuscator, + imageDataProvider: imageDataProvider ) let snapshot = ViewTreeSnapshot( date: recorderContext.date.addingTimeInterval(recorderContext.rumContext.viewServerTimeOffset ?? 0), @@ -47,7 +50,8 @@ extension ViewTreeSnapshotBuilder { self.init( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: defaultNodeRecorders), idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + textObfuscator: TextObfuscator(), + imageDataProvider: ImageDataProvider() ) } } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index d8faefbe99..df2670a3f7 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -277,7 +277,8 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { recorder: .mockRandom(), coordinateSpace: UIView.mockRandom(), ids: NodeIDGenerator(), - textObfuscator: TextObfuscator() + textObfuscator: TextObfuscator(), + imageDataProvider: MockImageDataProvider() ) } @@ -285,13 +286,15 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { recorder: Recorder.Context = .mockAny(), coordinateSpace: UICoordinateSpace = UIView.mockAny(), ids: NodeIDGenerator = NodeIDGenerator(), - textObfuscator: TextObfuscator = TextObfuscator() + textObfuscator: TextObfuscator = TextObfuscator(), + imageDataProvider: ImageDataProviding = MockImageDataProvider() ) -> ViewTreeRecordingContext { return .init( recorder: recorder, coordinateSpace: coordinateSpace, ids: ids, - textObfuscator: textObfuscator + textObfuscator: textObfuscator, + imageDataProvider: imageDataProvider ) } } diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift index 989fe9ab02..0cffd2ee10 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift @@ -18,7 +18,8 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + textObfuscator: TextObfuscator(), + imageDataProvider: ImageDataProvider() ) // When @@ -41,7 +42,8 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), idsGenerator: NodeIDGenerator(), - textObfuscator: TextObfuscator() + textObfuscator: TextObfuscator(), + imageDataProvider: MockImageDataProvider() ) // When From 36efe9036540b877a56e99439e33a573c3ce0e32 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 13 Mar 2023 11:47:21 +0000 Subject: [PATCH 23/72] REPLAY-1347 Fix linting --- .../NodeRecorders/UIImageViewWireframesBuilderTests.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index 5bfc56e8b5..5dcf029c67 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -8,18 +8,13 @@ import XCTest @testable import DatadogSessionReplay class UIImageViewWireframesBuilderTests: XCTestCase { - var wireframesBuilder: WireframesBuilder! + var wireframesBuilder: WireframesBuilder = .init() override func setUp() { super.setUp() wireframesBuilder = WireframesBuilder() } - override func tearDown() { - wireframesBuilder = nil - super.tearDown() - } - func test_BuildCorrectWireframes_fromValidData() { let wireframeID = WireframeID.mockRandom() let imageWireframeID = WireframeID.mockRandom() From dba14446e61b0f317d8b8185f803549cad91beb1 Mon Sep 17 00:00:00 2001 From: Rosa Trieu Date: Tue, 14 Mar 2023 12:57:16 -0700 Subject: [PATCH 24/72] full size image --- docs/rum_mobile_vitals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rum_mobile_vitals.md b/docs/rum_mobile_vitals.md index c5f2777b3f..b559dc6259 100644 --- a/docs/rum_mobile_vitals.md +++ b/docs/rum_mobile_vitals.md @@ -4,7 +4,7 @@ Real User Monitoring offers Mobile Vitals, a set of metrics inspired by [MetricK Mobile Vitals appear on your your application's **Performance Overview** page when you navigate to **UX Monitoring > Performance Monitoring** and click your application. From the mobile performance dashboard for your application, click on a graph in **Mobile Vitals** to apply a filter by version or examine filtered sessions. -{{< img src="real_user_monitoring/ios/ios-mobile-vitals-new.png" alt="Mobile Vitals in the Performance Tab" style="width:70%;">}} +{{< img src="real_user_monitoring/ios/ios-mobile-vitals-new.png" alt="Mobile Vitals in the Performance Tab" style="width:100%;">}} Understand your application's overall health and performance with the line graphs displaying metrics across various application versions. To filter on application version or see specific sessions and views, click on a graph. From f575c72daba66d04e1e4d6cba8f140512dd020b6 Mon Sep 17 00:00:00 2001 From: Pedro Lousada Date: Tue, 14 Mar 2023 14:02:06 +0000 Subject: [PATCH 25/72] [RUMM-2997] Send a crash to both RUM and Logging features --- .../Integrations/CrashReportSender.swift | 36 +++++++---- .../CrashReporting/CrashReporterTests.swift | 64 ++++++++++++++++--- .../Mocks/CrashReportingFeatureMocks.swift | 18 ++++++ 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift b/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift index b27589cbdf..d5c84a6c35 100644 --- a/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift +++ b/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift @@ -6,7 +6,7 @@ /// An object for sending crash reports. internal protocol CrashReportSender { - /// Send the crash report et context to integrations. + /// Send the crash report and context to integrations. /// /// - Parameters: /// - report: The crash report. @@ -53,30 +53,40 @@ internal struct MessageBusSender: CrashReportSender { "context": context ] ) + + sendLog( + baggage: [ + "report": report, + "context": context + ] + ) } private func sendRUM(baggage: FeatureBaggage) { core?.send( message: .custom(key: MessageKeys.crash, baggage: baggage), - else: { self.sendLog(baggage: baggage) } + else: { + DD.logger.warn( + """ + RUM Feature is not enabled. Will not send crash as RUM Error. + Make sure `.enableRUM(true)`when initializing Datadog SDK. + """ + ) + } ) } private func sendLog(baggage: FeatureBaggage) { core?.send( message: .custom(key: MessageKeys.crashLog, baggage: baggage), - else: printError - ) - } - - private func printError() { - // This case is not reachable in higher abstraction but we add sanity warning. - DD.logger.error( + else: { + DD.logger.warn( """ - In order to use Crash Reporting, RUM or Logging feature must be enabled. - Make sure `.enableRUM(true)` or `.enableLogging(true)` are configured - when initializing Datadog SDK. + Logging Feature is not enabled. Will not send crash as Log Error. + Make sure `.enableLogging(true)`when initializing Datadog SDK. """ - ) + ) + } + ) } } diff --git a/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift b/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift index 6296f2b1ef..94b05f144a 100644 --- a/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift +++ b/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift @@ -12,7 +12,7 @@ class CrashReporterTests: XCTestCase { // MARK: - Sending Crash Report func testWhenPendingCrashReportIsFound_itIsSentAndPurged() throws { - let expectation = self.expectation(description: "`LoggingOrRUMsender` sends the crash report") + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report") let crashContext: CrashContext = .mockRandom() let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) let plugin = CrashReportingPluginMock() @@ -46,6 +46,47 @@ class CrashReporterTests: XCTestCase { XCTAssertTrue(plugin.hasPurgedCrashReport == true, "It should ask to purge the crash report") } + func testWhenPendingCrashReportIsFound_itIsSentBothToRumAndLogs() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to both features") + let crashContext: CrashContext = .mockRandom() + let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let crashMessageReceiver = CrashMessageReceiverMock() + + let core = PassthroughCoreMock(messageReceiver: crashMessageReceiver) + + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = crashReport + plugin.injectedContextData = crashContext.data + + // When + let crashReporter = CrashReporter( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver() + ) + + //Then + plugin.didReadPendingCrashReport = { expectation.fulfill() } + crashReporter.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + + let sentRumBaggage = crashMessageReceiver.rumBaggage + let sentLogBaggage = crashMessageReceiver.logsBaggage + + XCTAssert(!sentRumBaggage.isEmpty, "RUM baggage must not be empty") + XCTAssert(!sentLogBaggage.isEmpty, "Log baggage must not be empty") + + DDAssertDictionariesEqual( + sentRumBaggage.attributes, + sentLogBaggage.attributes, + "RUM and logs baggage should be equal" + ) + } + func testWhenPendingCrashReportIsNotFound_itDoesNothing() { let expectation = self.expectation(description: "`plugin` checks the crash report") let plugin = CrashReportingPluginMock() @@ -74,7 +115,7 @@ class CrashReporterTests: XCTestCase { } func testWhenPendingCrashReportIsFoundButItHasUnavailableCrashContext_itPurgesTheCrashReportWithNoSending() { - let expectation = self.expectation(description: "`LoggingOrRUMsender` does not send the crash report") + let expectation = self.expectation(description: "`CrashReportSender` does not send the crash report") expectation.isInverted = true let plugin = CrashReportingPluginMock() @@ -260,13 +301,16 @@ class CrashReporterTests: XCTestCase { // Then waitForExpectations(timeout: 0.5, handler: nil) - XCTAssertEqual( - dd.logger.errorLog?.message, - """ - In order to use Crash Reporting, RUM or Logging feature must be enabled. - Make sure `.enableRUM(true)` or `.enableLogging(true)` are configured - when initializing Datadog SDK. - """ - ) + let logs = dd.logger.warnLogs + + XCTAssert(logs.contains(where: { $0.message == """ + Logging Feature is not enabled. Will not send crash as Log Error. + Make sure `.enableLogging(true)`when initializing Datadog SDK. + """ })) + + XCTAssert(logs.contains(where: { $0.message == """ + RUM Feature is not enabled. Will not send crash as RUM Error. + Make sure `.enableRUM(true)`when initializing Datadog SDK. + """ })) } } diff --git a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 134b970b79..5f12a9ad15 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -87,6 +87,24 @@ class CrashReportSenderMock: CrashReportSender { var didSendCrashReport: (() -> Void)? } +class CrashMessageReceiverMock: FeatureMessageReceiver { + var rumBaggage: FeatureBaggage = [:] + var logsBaggage: FeatureBaggage = [:] + + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + switch message { + case .custom(let key, let attributes) where key == "crash-log": + logsBaggage = attributes + return true + case .custom(let key, let attributes) where key == "crash": + rumBaggage = attributes + return true + default: + return false + } + } +} + extension CrashContext { static func mockAny() -> CrashContext { return mockWith() From 412f5fa14cf59c9f73e7f001ae513d0055a99760 Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Mon, 20 Mar 2023 14:00:43 -0400 Subject: [PATCH 26/72] RUMM-2872 Update RUM protocol schema Adds `is_active` to Session information. Default it to true for the time being. --- .../RUM/DataModels/RUMDataModels.swift | 22 ++++++++++++++++++- .../Integrations/CrashReportReceiver.swift | 2 ++ .../RUMMonitor/Scopes/RUMResourceScope.swift | 2 ++ .../Scopes/RUMUserActionScope.swift | 1 + .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 4 ++++ .../DatadogObjc/RUM/RUMDataModels+objc.swift | 22 ++++++++++++++++++- .../Datadog/Mocks/RUMDataModelMocks.swift | 11 +++++++++- 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift index 3aba87d398..273db9e84b 100644 --- a/Sources/Datadog/RUM/DataModels/RUMDataModels.swift +++ b/Sources/Datadog/RUM/DataModels/RUMDataModels.swift @@ -308,12 +308,16 @@ public struct RUMActionEvent: RUMDataModel { /// UUID of the session public let id: String + /// Whether this session is currently active. Set to false to manually stop a session + public let isActive: Bool? + /// Type of the session public let type: SessionType enum CodingKeys: String, CodingKey { case hasReplay = "has_replay" case id = "id" + case isActive = "is_active" case type = "type" } @@ -702,12 +706,16 @@ public struct RUMErrorEvent: RUMDataModel { /// UUID of the session public let id: String + /// Whether this session is currently active. Set to false to manually stop a session + public let isActive: Bool? + /// Type of the session public let type: SessionType enum CodingKeys: String, CodingKey { case hasReplay = "has_replay" case id = "id" + case isActive = "is_active" case type = "type" } @@ -963,12 +971,16 @@ public struct RUMLongTaskEvent: RUMDataModel { /// UUID of the session public let id: String + /// Whether this session is currently active. Set to false to manually stop a session + public let isActive: Bool? + /// Type of the session public let type: SessionType enum CodingKeys: String, CodingKey { case hasReplay = "has_replay" case id = "id" + case isActive = "is_active" case type = "type" } @@ -1387,12 +1399,16 @@ public struct RUMResourceEvent: RUMDataModel { /// UUID of the session public let id: String + /// Whether this session is currently active. Set to false to manually stop a session + public let isActive: Bool? + /// Type of the session public let type: SessionType enum CodingKeys: String, CodingKey { case hasReplay = "has_replay" case id = "id" + case isActive = "is_active" case type = "type" } @@ -1593,12 +1609,16 @@ public struct RUMViewEvent: RUMDataModel { /// UUID of the session public let id: String + /// Whether this session is currently active. Set to false to manually stop a session + public let isActive: Bool? + /// Type of the session public let type: SessionType enum CodingKeys: String, CodingKey { case hasReplay = "has_replay" case id = "id" + case isActive = "is_active" case type = "type" } @@ -2957,4 +2977,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/581880e6d9e9bb51f6c81ecd87bae2923865a2a5 +// Generated from https://github.com/DataDog/rum-events-format/tree/f964339c5f07f476dee5fc6a12b6c1214a40c1da diff --git a/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift b/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift index 01f03e4369..24b1f97d96 100644 --- a/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift +++ b/Sources/Datadog/RUM/Integrations/CrashReportReceiver.swift @@ -347,6 +347,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { session: .init( hasReplay: lastRUMView.session.hasReplay, id: lastRUMView.session.id, + isActive: true, type: lastRUMView.ciTest != nil ? .ciTest : .user ), source: lastRUMView.source?.toErrorEventSource ?? .ios, @@ -469,6 +470,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { session: .init( hasReplay: hasReplay, id: sessionUUID.toRUMDataFormat, + isActive: true, type: CITestIntegration.active != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift index 4583be071b..95b827dbe5 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift @@ -202,6 +202,7 @@ internal class RUMResourceScope: RUMScope { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -262,6 +263,7 @@ internal class RUMResourceScope: RUMScope { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift index 0d83038969..28f2d3e632 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift @@ -167,6 +167,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index e9d2597eaa..6580f8ee1f 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -396,6 +396,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -452,6 +453,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -559,6 +561,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -610,6 +613,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, + isActive: true, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift index eabab60602..3ac1102302 100644 --- a/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift +++ b/Sources/DatadogObjc/RUM/RUMDataModels+objc.swift @@ -666,6 +666,10 @@ public class DDRUMActionEventSession: NSObject { root.swiftModel.session.id } + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + @objc public var type: DDRUMActionEventSessionSessionType { .init(swift: root.swiftModel.session.type) } @@ -1628,6 +1632,10 @@ public class DDRUMErrorEventSession: NSObject { root.swiftModel.session.id } + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + @objc public var type: DDRUMErrorEventSessionSessionType { .init(swift: root.swiftModel.session.type) } @@ -2242,6 +2250,10 @@ public class DDRUMLongTaskEventSession: NSObject { root.swiftModel.session.id } + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + @objc public var type: DDRUMLongTaskEventSessionSessionType { .init(swift: root.swiftModel.session.type) } @@ -3173,6 +3185,10 @@ public class DDRUMResourceEventSession: NSObject { root.swiftModel.session.id } + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + @objc public var type: DDRUMResourceEventSessionSessionType { .init(swift: root.swiftModel.session.type) } @@ -3735,6 +3751,10 @@ public class DDRUMViewEventSession: NSObject { root.swiftModel.session.id } + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + @objc public var type: DDRUMViewEventSessionSessionType { .init(swift: root.swiftModel.session.type) } @@ -5091,4 +5111,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/581880e6d9e9bb51f6c81ecd87bae2923865a2a5 +// Generated from https://github.com/DataDog/rum-events-format/tree/f964339c5f07f476dee5fc6a12b6c1214a40c1da diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index 9bab1f6c48..cfa7985293 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -121,6 +121,7 @@ extension RUMViewEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), + isActive: true, type: .user ), source: .ios, @@ -219,6 +220,7 @@ extension RUMResourceEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), + isActive: true, type: .user ), source: .ios, @@ -272,6 +274,7 @@ extension RUMActionEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), + isActive: true, type: .user ), source: .ios, @@ -335,6 +338,7 @@ extension RUMErrorEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), + isActive: true, type: .user ), source: .ios, @@ -383,7 +387,12 @@ extension RUMLongTaskEvent: RandomMockable { longTask: .init(duration: .mockRandom(), id: .mockRandom(), isFrozenFrame: .mockRandom()), os: .mockRandom(), service: .mockRandom(), - session: .init(hasReplay: false, id: .mockRandom(), type: .user), + session: .init( + hasReplay: false, + id: .mockRandom(), + isActive: true, + type: .user + ), source: .ios, synthetics: nil, usr: .mockRandom(), From 4686c79f8111b13e272a4c27455fc92129bce626 Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Mon, 20 Mar 2023 17:04:16 -0400 Subject: [PATCH 27/72] RUMM-2872 Add session restart logic to ApplicationScope This requires ApplicationScope keep multiple sessions, and discard them as they're completed. --- .../Datadog/RUM/Debugging/RUMDebugging.swift | 2 +- .../Scopes/RUMApplicationScope.swift | 55 +++++-- .../RUMMonitor/Scopes/RUMResourceScope.swift | 4 +- .../Scopes/RUMUserActionScope.swift | 2 +- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 8 +- Sources/Datadog/RUMMonitor.swift | 9 +- .../Datadog/Mocks/RUMDataModelMocks.swift | 4 +- .../Scopes/RUMApplicationScopeTests.swift | 155 +++++++++++++++++- .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 21 ++- 9 files changed, 226 insertions(+), 34 deletions(-) diff --git a/Sources/Datadog/RUM/Debugging/RUMDebugging.swift b/Sources/Datadog/RUM/Debugging/RUMDebugging.swift index 8a976bc5c0..c165b1e2e4 100644 --- a/Sources/Datadog/RUM/Debugging/RUMDebugging.swift +++ b/Sources/Datadog/RUM/Debugging/RUMDebugging.swift @@ -21,7 +21,7 @@ private struct RUMDebugInfo { let views: [View] init(applicationScope: RUMApplicationScope) { - self.views = (applicationScope.sessionScope?.viewScopes ?? []) + self.views = (applicationScope.activeSession?.viewScopes ?? []) .map { View(scope: $0) } } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index d15c78e892..a01d63a445 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -11,11 +11,15 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // Whether the applciation is already active. Set to true // when the first session starts. - private(set) var appplicationActive: Bool = false + private(set) var applicationActive = false /// Session scope. It gets created with the first event. /// Might be re-created later according to session duration constraints. - private(set) var sessionScope: RUMSessionScope? + private(set) var sessionScopes: [RUMSessionScope] = [] + + var activeSession: RUMSessionScope? { + get { return sessionScopes.first(where: { $0.isActive }) } + } // MARK: - Initialization @@ -41,31 +45,50 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // MARK: - RUMScope func process(command: RUMCommand, context: DatadogContext, writer: Writer) -> Bool { - if sessionScope == nil { + if sessionScopes.isEmpty && !applicationActive { startInitialSession(on: command, context: context, writer: writer) } - if let currentSession = sessionScope { - sessionScope = sessionScope?.scope(byPropagating: command, context: context, writer: writer) + if activeSession == nil && command.isUserInteraction { + // No active sessions, start a new one + startSession(on: command, context: context, writer: writer) + } + + // Can't use scope(byPropagating:context:writer) because of the extra step in looking for sessions + // that need a refresh + sessionScopes = sessionScopes.compactMap({ scope in + if scope.process(command: command, context: context, writer: writer) { + // Returned true, keep the scope around, it still has work to do. + return scope + } - if sessionScope == nil { // if session expired - refresh(expiredSession: currentSession, on: command, context: context, writer: writer) + if scope.isActive { + // False, but still active means we timed out or expired, refresh the session + return refresh(expiredSession: scope, on: command, context: context, writer: writer) } + // Else, inactive and done processing events, remove + return nil + }) + + // Sanity telemety, only end up with one active session + if sessionScopes.filter({ $0.isActive }).count > 1 { + DD.telemetry.error("An application has multiple active sessions!") } - return true + return activeSession != nil } // MARK: - Private - private func refresh(expiredSession: RUMSessionScope, on command: RUMCommand, context: DatadogContext, writer: Writer) { + private func refresh(expiredSession: RUMSessionScope, on command: RUMCommand, context: DatadogContext, writer: Writer) -> RUMSessionScope { let refreshedSession = RUMSessionScope(from: expiredSession, startTime: command.time, context: context) - sessionScope = refreshedSession sessionScopeDidUpdate(refreshedSession) _ = refreshedSession.process(command: command, context: context, writer: writer) + return refreshedSession } private func startInitialSession(on command: RUMCommand, context: DatadogContext, writer: Writer) { + applicationActive = true let initialSession = RUMSessionScope( isInitialSession: true, parent: self, @@ -74,11 +97,11 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded ) - sessionScope = initialSession + sessionScopes.append(initialSession) sessionScopeDidUpdate(initialSession) if context.applicationStateHistory.currentSnapshot.state != .background { // Immediately start the ApplicationLaunchView for the new session - _ = sessionScope?.process( + _ = initialSession.process( command: RUMApplicationStartCommand( time: command.time, attributes: command.attributes @@ -89,6 +112,14 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { } } + private func startSession(on command: RUMCommand, context: DatadogContext, writer: Writer) { + let session = RUMSessionScope(isInitialSession: false, parent: self, startTime: command.time, dependencies: dependencies, isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded + ) + + sessionScopes.append(session) + sessionScopeDidUpdate(session) + } + private func sessionScopeDidUpdate(_ sessionScope: RUMSessionScope) { let sessionID = sessionScope.sessionUUID.rawValue.uuidString let isDiscarded = !sessionScope.isSampled diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift index 95b827dbe5..fdad7bea10 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMResourceScope.swift @@ -202,7 +202,7 @@ internal class RUMResourceScope: RUMScope { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: nil, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -263,7 +263,7 @@ internal class RUMResourceScope: RUMScope { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: nil, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift index 28f2d3e632..de99b27a41 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMUserActionScope.swift @@ -167,7 +167,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: nil, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 6580f8ee1f..5fde38201d 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -396,7 +396,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: self.context.isSessionActive, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -453,7 +453,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: self.context.isSessionActive, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -561,7 +561,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: self.context.isSessionActive, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, @@ -613,7 +613,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { session: .init( hasReplay: context.srBaggage?.isReplayBeingRecorded, id: self.context.sessionID.toRUMDataFormat, - isActive: true, + isActive: self.context.isSessionActive, type: dependencies.ciTest != nil ? .ciTest : .user ), source: .init(rawValue: context.source) ?? .ios, diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index 262e964ac2..cdc19d7f21 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -635,12 +635,11 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { // update the core context with rum context core.set(feature: "rum", attributes: { self.queue.sync { - let context = self.applicationScope.sessionScope?.viewScopes.last?.context ?? - self.applicationScope.sessionScope?.context ?? - self.applicationScope.context + let context = self.applicationScope.activeSession?.viewScopes.last?.context ?? + self.applicationScope.activeSession?.context ?? + self.applicationScope.context guard context.sessionID != .nullUUID else { - // if Session was sampled or not yet started return [:] } @@ -651,7 +650,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { RUMContextAttributes.IDs.viewID: context.activeViewID?.rawValue.uuidString.lowercased(), RUMContextAttributes.IDs.userActionID: context.activeUserActionID?.rawValue.uuidString.lowercased(), ], - RUMContextAttributes.serverTimeOffset: self.applicationScope.sessionScope?.viewScopes.last?.serverTimeOffset + RUMContextAttributes.serverTimeOffset: self.applicationScope.activeSession?.viewScopes.last?.serverTimeOffset ] } }) diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index cfa7985293..6971e71317 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -274,7 +274,7 @@ extension RUMActionEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), - isActive: true, + isActive: nil, type: .user ), source: .ios, @@ -338,7 +338,7 @@ extension RUMErrorEvent: RandomMockable { session: .init( hasReplay: nil, id: .mockRandom(), - isActive: true, + isActive: nil, type: .user ), source: .ios, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index a3367d626f..be0aa4e4d9 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -40,7 +40,7 @@ class RUMApplicationScopeTests: XCTestCase { onSessionStart: onSessionStart ) ) - XCTAssertNil(scope.sessionScope) + XCTAssertNil(scope.activeSession) // When let command = mockRandomRUMCommand().replacing(time: currentTime.addingTimeInterval(1)) @@ -49,7 +49,7 @@ class RUMApplicationScopeTests: XCTestCase { waitForExpectations(timeout: 0.5) // Then - let sessionScope = try XCTUnwrap(scope.sessionScope) + let sessionScope = try XCTUnwrap(scope.activeSession) XCTAssertTrue(sessionScope.isInitialSession, "Starting the very first view in application must create initial session") } @@ -79,7 +79,7 @@ class RUMApplicationScopeTests: XCTestCase { writer: writer ) - let initialSession = try XCTUnwrap(scope.sessionScope) + let initialSession = try XCTUnwrap(scope.activeSession) // When // Push time forward by the max session duration: @@ -93,7 +93,7 @@ class RUMApplicationScopeTests: XCTestCase { // Then waitForExpectations(timeout: 0.5) - let nextSession = try XCTUnwrap(scope.sessionScope) + let nextSession = try XCTUnwrap(scope.activeSession) XCTAssertNotEqual(initialSession.sessionUUID, nextSession.sessionUUID, "New session must have different id") XCTAssertEqual(initialSession.viewScopes.count, nextSession.viewScopes.count, "All view scopes must be transferred to the new session") @@ -181,4 +181,151 @@ class RUMApplicationScopeTests: XCTestCase { XCTAssertGreaterThan(trackedSessionsCount, halfSessionsCount * 0.8) // -20% XCTAssertLessThan(trackedSessionsCount, halfSessionsCount * 1.2) // +20% } + + // MARK: - Stopping and Restarting Sessions + + func testWhenStoppingSession_itHasNoActiveSesssion() throws { + // Given + var currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + + let command = mockRandomRUMCommand().replacing(time: currentTime.addingTimeInterval(1)) + XCTAssertTrue(scope.process(command: command, context: context, writer: writer)) + + // When + let stopCommand = RUMStopSessionCommand.mockAny() + XCTAssertFalse(scope.process(command: stopCommand, context: context, writer: writer)) + + // Then + XCTAssertNil(scope.activeSession) + } + + func testGivenStoppedSession_whenUserActionEvent_itStartsANewSession() throws { + // Given + var currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + XCTAssertTrue(scope.process( + command: RUMCommandMock(time: currentTime.addingTimeInterval(1), isUserInteraction: true), + context: context, + writer: writer + )) + XCTAssertFalse(scope.process( + command: RUMStopSessionCommand.mockWith(time: currentTime.addingTimeInterval(2)), + context: context, + writer: writer + )) + XCTAssertTrue(scope.process( + command: RUMCommandMock(time: currentTime.addingTimeInterval(3), isUserInteraction: true), + context: context, + writer: writer + )) + + // Then + XCTAssertEqual(scope.sessionScopes.count, 1) + XCTAssertNotNil(scope.activeSession) + } + + func testGivenStoppedSession_whenNonUserIntaractionEvent_itDoesNotStartANewSession() throws { + // Given + var currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + XCTAssertTrue(scope.process( + command: RUMCommandMock(time: currentTime.addingTimeInterval(1), isUserInteraction: true), + context: context, + writer: writer + )) + XCTAssertFalse(scope.process( + command: RUMStopSessionCommand.mockWith(time: currentTime.addingTimeInterval(2)), + context: context, + writer: writer + )) + XCTAssertFalse(scope.process( + command: RUMCommandMock(time: currentTime.addingTimeInterval(3), isUserInteraction: false), + context: context, + writer: writer + )) + + // Then + XCTAssertEqual(scope.sessionScopes.count, 0) + XCTAssertNil(scope.activeSession) + } + + func testGivenStoppedSessionProcessingResources_itCanStayInactive() throws { + // Given + var currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + XCTAssertTrue(scope.process( + command: RUMStartResourceCommand.mockRandom(), + context: context, + writer: writer + )) + XCTAssertFalse(scope.process( + command: RUMStopSessionCommand.mockWith(time: currentTime.addingTimeInterval(2)), + context: context, + writer: writer + )) + + // Then + XCTAssertEqual(scope.sessionScopes.count, 1) + XCTAssertNil(scope.activeSession) + } + + func testGivenStoppedSessionProcessingResources_itIsRemovedWhenFinished() throws { + // Given + var currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + let resourceKey = "resources/1" + XCTAssertTrue(scope.process( + command: RUMStartResourceCommand.mockWith( + resourceKey: resourceKey, + time: currentTime.addingTimeInterval(1) + ), + context: context, + writer: writer + )) + let firstSession = scope.activeSession + XCTAssertFalse(scope.process( + command: RUMStopSessionCommand.mockWith(time: currentTime.addingTimeInterval(2)), + context: context, + writer: writer + )) + XCTAssertTrue(scope.process( + command: RUMCommandMock(time: currentTime.addingTimeInterval(3), isUserInteraction: true), + context: context, + writer: writer + )) + let secondSession = scope.activeSession + XCTAssertTrue(scope.process( + command: RUMStopResourceCommand.mockWith( + resourceKey: resourceKey, + time: currentTime.addingTimeInterval(4) + ), + context: context, + writer: writer + )) + + // Then + XCTAssertEqual(scope.sessionScopes.count, 1) + XCTAssertEqual(scope.activeSession?.sessionUUID, secondSession?.sessionUUID) + } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index c3f588e359..939726ee23 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -1608,11 +1608,9 @@ class RUMViewScopeTests: XCTestCase { // MARK: - Stopped Session - func testGivenSession_whenSessionStopped_itSendsViewUpdateWithStopped() { + func testGivenSession_whenSessionStopped_itSendsViewUpdateWithStopped() throws { let initialDeviceTime: Date = .mockDecember15th2019At10AMUTC() let initialServerTimeOffset: TimeInterval = 120 // 2 minutes - var currentDeviceTime = initialDeviceTime - // Given let scope = RUMViewScope( @@ -1627,6 +1625,23 @@ class RUMViewScopeTests: XCTestCase { startTime: initialDeviceTime, serverTimeOffset: initialServerTimeOffset ) + parent.context.isSessionActive = false + + // When + XCTAssertFalse( + scope.process( + command: RUMStopSessionCommand.mockAny(), + context: context, + writer: writer + ) + ) + + // Then + XCTAssertFalse(scope.isActiveView) + let events = try XCTUnwrap(writer.events(ofType: RUMViewEvent.self)) + + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first?.session.isActive, false) } // MARK: - Dates Correction From 6cf09caeb64c60b785f652a042e1ad5cef0df8b5 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 21 Mar 2023 11:13:26 +0000 Subject: [PATCH 28/72] Change initial sample collecting --- Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift b/Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift index dfee4bdbbc..aab6c053b9 100644 --- a/Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift +++ b/Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift @@ -63,6 +63,10 @@ internal final class VitalInfoSampler { self.refreshRateReader.register(self.refreshRatePublisher) self.maximumRefreshRate = maximumRefreshRate + // Take initial sample + RunLoop.main.perform(inModes: [.common]) { [weak self] in + self?.takeSample() + } // Schedule reoccuring samples let timer = Timer( timeInterval: frequency, @@ -70,13 +74,6 @@ internal final class VitalInfoSampler { ) { [weak self] _ in self?.takeSample() } - // Take initial sample - RunLoop.main.perform { - timer.fire() - } - // NOTE: RUMM-1280 based on my running Example app - // non-main run loops don't fire the timer. - // Although i can't catch this in unit tests RunLoop.main.add(timer, forMode: .common) self.timer = timer } From 682c34053b1df791db7a64398f6868d66042c437 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 21 Mar 2023 13:34:32 +0100 Subject: [PATCH 29/72] REPLAY-1330 CR feedback - update SnapshotTest deployment target to iOS 13.0 --- .../SRSnapshotTests.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index 4fdcc08e3f..37e4b14868 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -443,7 +443,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -471,7 +471,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -496,7 +496,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JKFCB4CN7C; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SRSnapshotTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -514,7 +514,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = JKFCB4CN7C; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.SRSnapshotTests; PRODUCT_NAME = "$(TARGET_NAME)"; From 236555d70f11c900c37934412e36425601b21771 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 21 Mar 2023 13:44:52 +0100 Subject: [PATCH 30/72] REPLAY-1330 CR feedback - ignore `_UICutoutShadowView` uniformly in all image recorders --- .../NodeRecorders/UIImageViewRecorder.swift | 8 +++++++- .../ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift | 11 +---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 3e0e48dcbb..e7dee5c9e0 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -8,7 +8,13 @@ import UIKit internal class UIImageViewRecorder: NodeRecorder { /// An option for overriding default semantics from parent recorder. - var semanticsOverride: (UIImageView, ViewAttributes) -> NodeSemantics? = { _, _ in nil } + var semanticsOverride: (UIImageView, ViewAttributes) -> NodeSemantics? = { imageView, _ in + let className = "\(type(of: imageView))" + // This gets effective on iOS 15.0+ which is the earliest version that displays some elements in popover views. + // Here we explicitly ignore the "shadow" effect applied to popover. + let isSystemShadow = className == "_UICutoutShadowView" + return isSystemShadow ? IgnoredElement(subtreeStrategy: .ignore) : nil + } private let imageDataProvider = ImageDataProvider() diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 8baf944ea8..00571257a9 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -67,19 +67,10 @@ extension ViewTreeSnapshotBuilder { /// An arrays of default node recorders executed for the root view-tree hierarchy. internal func createDefaultNodeRecorders() -> [NodeRecorder] { - let imageViewRecorder = UIImageViewRecorder() - imageViewRecorder.semanticsOverride = { imageView, _ in - let className = "\(type(of: imageView))" - // This gets effective on iOS 15.0+ which is the earliest version that displays some elements in popover views. - // Here we explicitly ignore the "shadow" effect applied to popover. - let isSystemShadow = className == "_UICutoutShadowView" - return isSystemShadow ? IgnoredElement(subtreeStrategy: .ignore) : nil - } - return [ UIViewRecorder(), UILabelRecorder(), - imageViewRecorder, + UIImageViewRecorder(), UITextFieldRecorder(), UITextViewRecorder(), UISwitchRecorder(), From c539ee6a3393f9bd21fe83ad40cb853910b0675e Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 21 Mar 2023 13:29:39 +0000 Subject: [PATCH 31/72] REPLAY-1347 PR fixes --- .../Recorder/Utilities/ImageDataProvider.swift | 6 +----- .../Tests/Mocks/MockImageDataProvider.swift | 14 ++++++++++++++ .../NodeRecorders/UIImageViewRecorderTests.swift | 2 +- .../UIImageViewWireframesBuilderTests.swift | 6 ------ 4 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 3f813b4593..f83ba9c139 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -21,14 +21,10 @@ internal protocol ImageDataProviding { internal class ImageDataProvider: ImageDataProviding { private var cache: Cache - private let maxBytesSize: Int - internal init( - cache: Cache = .init(), - maxBytesSize: Int = 10_000 + cache: Cache = .init() ) { self.cache = cache - self.maxBytesSize = maxBytesSize } func contentBase64String( diff --git a/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift b/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift new file mode 100644 index 0000000000..9cbc1d2a0f --- /dev/null +++ b/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift @@ -0,0 +1,14 @@ +/* + * 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. + */ + +import UIKit +@testable import DatadogSessionReplay + +class MockImageDataProvider: ImageDataProvider { + override func contentBase64String(of image: UIImage?, tintColor: UIColor?) -> String { + return "mock_base64_string" + } +} diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift index ee25e5f199..dabd8e059b 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift @@ -65,7 +65,7 @@ class UIImageViewRecorderTests: XCTestCase { XCTAssertFalse(builder.shouldRecordImage) } - func testWhenTintColorIsProvider() throws { + func testWhenTintColorIsProvided() throws { // When let recorder = UIImageViewRecorder(tintColorProvider: { _ in return .red }) imageView.image = UIImage() diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index 5dcf029c67..761ea3036a 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -81,9 +81,3 @@ class UIImageViewWireframesBuilderTests: XCTestCase { } } } - -class MockImageDataProvider: ImageDataProvider { - override func contentBase64String(of image: UIImage?, tintColor: UIColor?) -> String { - return "mock_base64_string" - } -} From 35574233697d030ceec05c767c52c1afe6e5acec Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 21 Mar 2023 17:35:50 +0100 Subject: [PATCH 32/72] REPLAY-1457 Enhance `UISwitch` element masking so that selected value cannot be seen if `.maskAll` strategy is set. --- .../NodeRecorders/UISwitchRecorder.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift index 5d3b089151..992ae06277 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift @@ -29,6 +29,7 @@ internal struct UISwitchRecorder: NodeRecorder { isEnabled: `switch`.isEnabled, isDarkMode: `switch`.usesDarkMode, isOn: `switch`.isOn, + isMasked: context.recorder.privacy == .maskAll, thumbTintColor: `switch`.thumbTintColor?.cgColor, onTintColor: `switch`.onTintColor?.cgColor, offTintColor: `switch`.tintColor?.cgColor @@ -48,11 +49,41 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { let isEnabled: Bool let isDarkMode: Bool let isOn: Bool + let isMasked: Bool let thumbTintColor: CGColor? let onTintColor: CGColor? let offTintColor: CGColor? func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { + if isMasked { + return createMasked(with: builder) + } else { + return createNotMasked(with: builder) + } + } + + private func createMasked(with builder: WireframesBuilder) -> [SRWireframe] { + let track = builder.createShapeWireframe( + id: trackWireframeID, + frame: wireframeRect, + borderColor: nil, + borderWidth: nil, + backgroundColor: SystemColors.tertiarySystemFill, + cornerRadius: wireframeRect.height * 0.5, + opacity: isEnabled ? attributes.alpha : 0.5 + ) + + // Create background wireframe if the underlying `UIView` has any appearance: + if attributes.hasAnyAppearance { + let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: attributes.frame, attributes: attributes) + + return [background, track] + } else { + return [track] + } + } + + private func createNotMasked(with builder: WireframesBuilder) -> [SRWireframe] { let radius = wireframeRect.height * 0.5 // Create track wireframe: From dfbcee63940169f9c165989f86e26a708fe0d094 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 21 Mar 2023 17:46:19 +0100 Subject: [PATCH 33/72] REPLAY-1460 Enhance `UIStepper` element masking so that min and max values cannot be seen if `.maskAll` strategy is set. --- .../ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index 40fc7eb2bf..33f3fa78d5 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -17,6 +17,7 @@ internal struct UIStepperRecorder: NodeRecorder { let stepperFrame = CGRect(origin: attributes.frame.origin, size: stepper.intrinsicContentSize) let ids = context.ids.nodeIDs(5, for: stepper) + let isMasked = context.recorder.privacy == .maskAll let builder = UIStepperWireframesBuilder( wireframeRect: stepperFrame, @@ -26,8 +27,8 @@ internal struct UIStepperRecorder: NodeRecorder { minusWireframeID: ids[2], plusHorizontalWireframeID: ids[3], plusVerticalWireframeID: ids[4], - isMinusEnabled: stepper.value > stepper.minimumValue, - isPlusEnabled: stepper.value < stepper.maximumValue + isMinusEnabled: isMasked || (stepper.value > stepper.minimumValue), + isPlusEnabled: isMasked || (stepper.value < stepper.maximumValue) ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return SpecificElement(subtreeStrategy: .ignore, nodes: [node]) From ad2f290944e2c120d316767eab86957a5a8e3dec Mon Sep 17 00:00:00 2001 From: Pedro Lousada Date: Wed, 22 Mar 2023 11:00:29 +0000 Subject: [PATCH 34/72] Rework logic to only use a single message on the message BUS --- .../Integrations/CrashReportSender.swift | 38 ++----------- .../Logging/LoggingV2Configuration.swift | 2 +- .../CrashReporting/CrashReporterTests.swift | 57 ++++++++++++------- .../Mocks/CrashReportingFeatureMocks.swift | 24 +++++--- 4 files changed, 61 insertions(+), 60 deletions(-) diff --git a/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift b/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift index d5c84a6c35..7bc473c057 100644 --- a/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift +++ b/Sources/Datadog/CrashReporting/Integrations/CrashReportSender.swift @@ -21,14 +21,8 @@ internal struct MessageBusSender: CrashReportSender { /// The key for a crash message. /// /// Use this key when the crash should be reported - /// as a RUM event. + /// as a RUM and a Logs event. static let crash = "crash" - - /// The key for a crash log message. - /// - /// Use this key when the crash should be reported - /// as a log event. - static let crashLog = "crash-log" } /// The core for sending crash report and context. @@ -47,14 +41,7 @@ internal struct MessageBusSender: CrashReportSender { return } - sendRUM( - baggage: [ - "report": report, - "context": context - ] - ) - - sendLog( + sendCrash( baggage: [ "report": report, "context": context @@ -62,31 +49,18 @@ internal struct MessageBusSender: CrashReportSender { ) } - private func sendRUM(baggage: FeatureBaggage) { + private func sendCrash(baggage: FeatureBaggage) { core?.send( message: .custom(key: MessageKeys.crash, baggage: baggage), else: { DD.logger.warn( """ - RUM Feature is not enabled. Will not send crash as RUM Error. - Make sure `.enableRUM(true)`when initializing Datadog SDK. + In order to use Crash Reporting, RUM or Logging feature must be enabled. + Make sure `.enableRUM(true)` or `.enableLogging(true)` are configured + when initializing Datadog SDK. """ ) } ) } - - private func sendLog(baggage: FeatureBaggage) { - core?.send( - message: .custom(key: MessageKeys.crashLog, baggage: baggage), - else: { - DD.logger.warn( - """ - Logging Feature is not enabled. Will not send crash as Log Error. - Make sure `.enableLogging(true)`when initializing Datadog SDK. - """ - ) - } - ) - } } diff --git a/Sources/Datadog/Logging/LoggingV2Configuration.swift b/Sources/Datadog/Logging/LoggingV2Configuration.swift index c865c77067..e91f67b4ac 100644 --- a/Sources/Datadog/Logging/LoggingV2Configuration.swift +++ b/Sources/Datadog/Logging/LoggingV2Configuration.swift @@ -67,7 +67,7 @@ internal enum LoggingMessageKeys { static let log = "log" /// The key references a crash message. - static let crash = "crash-log" + static let crash = "crash" /// The key references a browser log message. static let browserLog = "browser-log" diff --git a/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift b/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift index 94b05f144a..20468fb422 100644 --- a/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift +++ b/Tests/DatadogTests/Datadog/CrashReporting/CrashReporterTests.swift @@ -46,13 +46,13 @@ class CrashReporterTests: XCTestCase { XCTAssertTrue(plugin.hasPurgedCrashReport == true, "It should ask to purge the crash report") } - func testWhenPendingCrashReportIsFound_itIsSentBothToRumAndLogs() throws { - let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to both features") + func testWhenPendingCrashReportIsFound_itIsSentToRumFeature() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to RUM feature") let crashContext: CrashContext = .mockRandom() let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) - let crashMessageReceiver = CrashMessageReceiverMock() + let rumCrashReceiver = RUMCrashReceiverMock() - let core = PassthroughCoreMock(messageReceiver: crashMessageReceiver) + let core = PassthroughCoreMock(messageReceiver: rumCrashReceiver) let plugin = CrashReportingPluginMock() @@ -74,17 +74,38 @@ class CrashReporterTests: XCTestCase { waitForExpectations(timeout: 0.5, handler: nil) - let sentRumBaggage = crashMessageReceiver.rumBaggage - let sentLogBaggage = crashMessageReceiver.logsBaggage + XCTAssert(!rumCrashReceiver.receivedBaggage.isEmpty, "RUM baggage must not be empty") + } - XCTAssert(!sentRumBaggage.isEmpty, "RUM baggage must not be empty") - XCTAssert(!sentLogBaggage.isEmpty, "Log baggage must not be empty") + func testWhenPendingCrashReportIsFound_itIsSentToLogsFeature() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to Logs feature") + let crashContext: CrashContext = .mockRandom() + let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let logsCrashReceiver = LogsCrashReceiverMock() - DDAssertDictionariesEqual( - sentRumBaggage.attributes, - sentLogBaggage.attributes, - "RUM and logs baggage should be equal" + let core = PassthroughCoreMock(messageReceiver: logsCrashReceiver) + + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = crashReport + plugin.injectedContextData = crashContext.data + + // When + let crashReporter = CrashReporter( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver() ) + + //Then + plugin.didReadPendingCrashReport = { expectation.fulfill() } + crashReporter.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + + XCTAssert(!logsCrashReceiver.receivedBaggage.isEmpty, "Logs baggage must not be empty") } func testWhenPendingCrashReportIsNotFound_itDoesNothing() { @@ -274,7 +295,7 @@ class CrashReporterTests: XCTestCase { // MARK: - Usage - func testGivenNoRegisteredCrashReportReceiver_whenPendingCrashReportIsFound_itPrintsError() { + func testGivenNoRegisteredCrashReportReceiver_whenPendingCrashReportIsFound_itPrintsWarning() { let expectation = self.expectation(description: "`plugin` checks the crash report") let dd = DD.mockWith(logger: CoreLoggerMock()) @@ -304,13 +325,9 @@ class CrashReporterTests: XCTestCase { let logs = dd.logger.warnLogs XCTAssert(logs.contains(where: { $0.message == """ - Logging Feature is not enabled. Will not send crash as Log Error. - Make sure `.enableLogging(true)`when initializing Datadog SDK. - """ })) - - XCTAssert(logs.contains(where: { $0.message == """ - RUM Feature is not enabled. Will not send crash as RUM Error. - Make sure `.enableRUM(true)`when initializing Datadog SDK. + In order to use Crash Reporting, RUM or Logging feature must be enabled. + Make sure `.enableRUM(true)` or `.enableLogging(true)` are configured + when initializing Datadog SDK. """ })) } } diff --git a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 5f12a9ad15..6c4fdfd0ab 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -87,17 +87,27 @@ class CrashReportSenderMock: CrashReportSender { var didSendCrashReport: (() -> Void)? } -class CrashMessageReceiverMock: FeatureMessageReceiver { - var rumBaggage: FeatureBaggage = [:] - var logsBaggage: FeatureBaggage = [:] +class RUMCrashReceiverMock: FeatureMessageReceiver { + var receivedBaggage: FeatureBaggage = [:] func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { switch message { - case .custom(let key, let attributes) where key == "crash-log": - logsBaggage = attributes + case .custom(let key, let attributes) where key == CrashReportReceiver.MessageKeys.crash: + receivedBaggage = attributes return true - case .custom(let key, let attributes) where key == "crash": - rumBaggage = attributes + default: + return false + } + } +} + +class LogsCrashReceiverMock: FeatureMessageReceiver { + var receivedBaggage: FeatureBaggage = [:] + + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + switch message { + case .custom(let key, let attributes) where key == LoggingMessageKeys.crash: + receivedBaggage = attributes return true default: return false From 93db921abb92aabc821f06c1353a28c475107e1a Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Tue, 21 Mar 2023 17:12:15 -0400 Subject: [PATCH 35/72] RUMM-2872 Add integration tests for stopped sessions --- Datadog/Datadog.xcodeproj/project.pbxproj | 36 +++- ...UMManualInstrumentationScenario.storyboard | 32 +-- .../Example/Scenarios/RUM/RUMScenarios.swift | 7 + .../KioskSendEventsViewController.swift | 60 ++++++ ...kSendInterruptedEventsViewController.swift | 49 +++++ .../KioskViewController.swift | 23 +++ .../RUMStopSessionScenario.storyboard | 183 ++++++++++++++++++ .../Scopes/RUMApplicationScope.swift | 7 +- .../RUMMonitor/Scopes/RUMSessionScope.swift | 7 +- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 2 +- .../Scenarios/RUM/RUMCommonAsserts.swift | 7 +- .../RUM/RUMStopSessionScenarioTests.swift | 137 +++++++++++++ 12 files changed, 525 insertions(+), 25 deletions(-) create mode 100644 Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift create mode 100644 Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift create mode 100644 Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.swift create mode 100644 Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard create mode 100644 Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index d5e943af67..c620b1bfe1 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 490D5EC929C9E17E004F969C /* RUMStopSessionScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */; }; + 490D5ECF29CA0745004F969C /* RUMStopSessionScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */; }; + 490D5ED029CA074A004F969C /* KioskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ECD29CA0738004F969C /* KioskViewController.swift */; }; + 490D5ED329CA08F7004F969C /* KioskSendEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */; }; + 490D5ED629CA1DD6004F969C /* KioskSendInterruptedEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */; }; 49274906288048B500ECD49B /* InternalTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */; }; 49274907288048B800ECD49B /* InternalTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */; }; 4927490B288048FF00ECD49B /* RUMInternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */; }; @@ -1350,6 +1355,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMStopSessionScenarioTests.swift; sourceTree = ""; }; + 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMStopSessionScenario.storyboard; sourceTree = ""; }; + 490D5ECD29CA0738004F969C /* KioskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskViewController.swift; sourceTree = ""; }; + 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSendEventsViewController.swift; sourceTree = ""; }; + 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSendInterruptedEventsViewController.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalTelemetryTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; 61020C292757AD91005EEAEA /* BackgroundLocationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundLocationMonitor.swift; sourceTree = ""; }; @@ -2188,6 +2198,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 490D5ECA29C9E28F004F969C /* StopSessionScenario */ = { + isa = PBXGroup; + children = ( + 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */, + 490D5ECD29CA0738004F969C /* KioskViewController.swift */, + 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */, + 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */, + ); + path = StopSessionScenario; + sourceTree = ""; + }; 61020C272757AD63005EEAEA /* BackgroundEvents */ = { isa = PBXGroup; children = ( @@ -2852,14 +2873,15 @@ isa = PBXGroup; children = ( 61D50C532580EF41006038A3 /* RUMScenarios.swift */, - 9EC2835C26CFF56B00FACF1C /* MobileVitals */, 61337037250F84FD00236D58 /* ManualInstrumentation */, + 9EC2835C26CFF56B00FACF1C /* MobileVitals */, + 6137E647252DD85400720485 /* ModalViewsAutoInstrumentation */, 61F9CA7725125918000A5E61 /* NavigationControllerAutoInstrumentation */, + 612D8F6725AEE65F000E2E09 /* Scrubbing */, + 490D5ECA29C9E28F004F969C /* StopSessionScenario */, + D2791EF32716F16E0046E07A /* SwiftUIInstrumentation */, 615AAC34251E34EF00C89EE9 /* TabBarAutoInstrumentation */, - 6137E647252DD85400720485 /* ModalViewsAutoInstrumentation */, 6193DCA2251B5669009B8011 /* TapActionAutoInstrumentation */, - D2791EF32716F16E0046E07A /* SwiftUIInstrumentation */, - 612D8F6725AEE65F000E2E09 /* Scrubbing */, ); path = RUM; sourceTree = ""; @@ -3954,6 +3976,7 @@ 612D8F8025AF1C74000E2E09 /* RUMScrubbingScenarioTests.swift */, 9EC2835926CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift */, D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */, + 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */, ); path = RUM; sourceTree = ""; @@ -5133,6 +5156,7 @@ 61337032250F82AE00236D58 /* LoggingManualInstrumentationScenario.storyboard in Resources */, 612D8F6925AEE68F000E2E09 /* RUMScrubbingScenario.storyboard in Resources */, D2F5BB36271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard in Resources */, + 490D5ECF29CA0745004F969C /* RUMStopSessionScenario.storyboard in Resources */, 611EA13C2580F77400BC0E56 /* TrackingConsentScenario.storyboard in Resources */, 9EA95C1D2791C9BE00F6C1F3 /* WebViewTrackingScenario.storyboard in Resources */, 61337039250F852E00236D58 /* RUMManualInstrumentationScenario.storyboard in Resources */, @@ -5938,6 +5962,7 @@ 618DCFE124C766F500589570 /* SendRUMFixture2ViewController.swift in Sources */, 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, 9EA95C1E2791C9BE00F6C1F3 /* WebViewScenarios.swift in Sources */, + 490D5ED329CA08F7004F969C /* KioskSendEventsViewController.swift in Sources */, 61F9CA8025125C01000A5E61 /* RUMNCSScreen3ViewController.swift in Sources */, 6164AE89252B4ECA000D78C4 /* SendThirdPartyRequestsViewController.swift in Sources */, 611EA14225810E1900BC0E56 /* TSHomeViewController.swift in Sources */, @@ -5946,12 +5971,14 @@ 61163C37252DDD60007DD5BF /* RUMMVSViewController.swift in Sources */, 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */, 61D50C5A2580EFF3006038A3 /* URLSessionScenarios.swift in Sources */, + 490D5ED029CA074A004F969C /* KioskViewController.swift in Sources */, 618236892710560900125326 /* DebugWebviewViewController.swift in Sources */, 614CADCE250FCA0200B93D2D /* TestScenarios.swift in Sources */, 6111542525C992F8007C84C9 /* CrashReportingScenarios.swift in Sources */, 61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */, 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */, 61098D2B27FEE3F00021237A /* MessagePortChannel.swift in Sources */, + 490D5ED629CA1DD6004F969C /* KioskSendInterruptedEventsViewController.swift in Sources */, 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */, 6111543F25C996A5007C84C9 /* CrashReportingViewController.swift in Sources */, 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */, @@ -6009,6 +6036,7 @@ 9EA95C212791C9E200F6C1F3 /* WebViewScenarioTest.swift in Sources */, 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */, 6167ACFD251A22E00012B4D0 /* TracingURLSessionScenarioTests.swift in Sources */, + 490D5EC929C9E17E004F969C /* RUMStopSessionScenarioTests.swift in Sources */, 611EA16625825FB300BC0E56 /* TrackingConsentScenarioTests.swift in Sources */, 61F3CD9A2510D8C500C816E5 /* Environment.swift in Sources */, 61F9CA9F2513978D000A5E61 /* RUMSessionMatcher.swift in Sources */, diff --git a/Datadog/Example/Scenarios/RUM/ManualInstrumentation/RUMManualInstrumentationScenario.storyboard b/Datadog/Example/Scenarios/RUM/ManualInstrumentation/RUMManualInstrumentationScenario.storyboard index 086ef0ddf3..d93563669f 100644 --- a/Datadog/Example/Scenarios/RUM/ManualInstrumentation/RUMManualInstrumentationScenario.storyboard +++ b/Datadog/Example/Scenarios/RUM/ManualInstrumentation/RUMManualInstrumentationScenario.storyboard @@ -1,10 +1,11 @@ - + - + + @@ -17,7 +18,7 @@ - + + @@ -77,7 +79,6 @@ -
@@ -97,7 +98,7 @@ - + + -
@@ -150,19 +151,19 @@ - + + -
@@ -175,7 +176,7 @@ - + @@ -187,4 +188,9 @@
+ + + + + diff --git a/Datadog/Example/Scenarios/RUM/RUMScenarios.swift b/Datadog/Example/Scenarios/RUM/RUMScenarios.swift index 5d7486fa38..8e3ed37962 100644 --- a/Datadog/Example/Scenarios/RUM/RUMScenarios.swift +++ b/Datadog/Example/Scenarios/RUM/RUMScenarios.swift @@ -251,6 +251,13 @@ final class RUMScrubbingScenario: TestScenario { } } + +/// Scenario which starts a navigation controller. Each view controller pushed to this navigation +/// uses the RUM manual instrumentation API to send RUM events to the server. +final class RUMStopSessionsScenario: TestScenario { + static let storyboardName = "RUMStopSessionScenario" +} + @available(iOS 13, *) /// Scenario which presents `SwiftUI`-based hierarchy and navigates through /// its views and view controllers. diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift new file mode 100644 index 0000000000..aeb6a789ea --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift @@ -0,0 +1,60 @@ +// 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 2023-Present Datadog, Inc. + +import UIKit + +internal class KioskSendEventsViewController: UIViewController { + @IBOutlet private var doneButton: UIButton! + + + override func viewDidLoad() { + super.viewDidLoad() + + doneButton.isHidden = true + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + rumMonitor.startView(viewController: self, name: "KioskSendEvents") + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + rumMonitor.stopView(viewController: self) + } + + @IBAction func didTapDownloadResourceButton(_ sender: Any) { + rumMonitor.addUserAction( + type: .tap, + name: (sender as! UIButton).currentTitle!, + attributes: ["button.description": String(describing: sender)] + ) + + let simulatedResourceKey = "/resource/1" + let simulatedResourceRequest = URLRequest(url: URL(string: "https://foo.com/resource/1")!) + let simulatedResourceLoadingTime: TimeInterval = 0.1 + + rumMonitor.startResourceLoading( + resourceKey: simulatedResourceKey, + request: simulatedResourceRequest + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + simulatedResourceLoadingTime) { + rumMonitor.stopResourceLoading( + resourceKey: simulatedResourceKey, + response: HTTPURLResponse( + url: simulatedResourceRequest.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "image/png"] + )! + ) + + // Reveal "Done" so UITest can continue + self.doneButton.isHidden = false + } + } +} diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift new file mode 100644 index 0000000000..a343473893 --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift @@ -0,0 +1,49 @@ +// 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 2023-Present Datadog, Inc. + +import UIKit + +internal class KioskSendInterruptedEventsViewController: UIViewController { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + rumMonitor.startView(viewController: self, name: "KioskSendInterruptedEvents") + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + rumMonitor.stopView(viewController: self) + } + + @IBAction func didTapDownloadResourceButton(_ sender: Any) { + rumMonitor.addUserAction( + type: .tap, + name: (sender as! UIButton).currentTitle!, + attributes: ["button.description": String(describing: sender)] + ) + + let simulatedResourceKey = "/resource/1" + let simulatedResourceRequest = URLRequest(url: URL(string: "https://foo.com/resource/1")!) + // Much longer wait time + let simulatedResourceLoadingTime: TimeInterval = 1.0 + + rumMonitor.startResourceLoading( + resourceKey: simulatedResourceKey, + request: simulatedResourceRequest + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + simulatedResourceLoadingTime) { + rumMonitor.stopResourceLoading( + resourceKey: simulatedResourceKey, + response: HTTPURLResponse( + url: simulatedResourceRequest.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "image/png"] + )! + ) + } + } +} diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.swift b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.swift new file mode 100644 index 0000000000..8f0d0cae36 --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.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 2023-Present Datadog, Inc. + +import UIKit + +internal class KioskViewController: UIViewController { + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + rumMonitor.startView(viewController: self, name: "KioskViewController") + + // Stop session + rumMonitor.stopSession() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + rumMonitor.stopView(viewController: self) + } +} diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard b/Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard new file mode 100644 index 0000000000..1d3add9c1f --- /dev/null +++ b/Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index a01d63a445..7b69d7fa65 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -113,7 +113,12 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { } private func startSession(on command: RUMCommand, context: DatadogContext, writer: Writer) { - let session = RUMSessionScope(isInitialSession: false, parent: self, startTime: command.time, dependencies: dependencies, isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded + let session = RUMSessionScope( + isInitialSession: false, + parent: self, + startTime: command.time, + dependencies: dependencies, + isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded ) sessionScopes.append(session) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 176c3cd52e..a4de8e6487 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -150,6 +150,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { var deactivating = false if isActive { if command is RUMStopSessionCommand { + isActive = false deactivating = true } else if let startApplicationCommand = command as? RUMApplicationStartCommand { startApplicationLaunchView(on: startApplicationCommand, context: context, writer: writer) @@ -185,7 +186,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Propagate command viewScopes = viewScopes.scopes(byPropagating: command, context: context, writer: writer) - if isActive && !hasActiveView { + if (isActive || deactivating) && !hasActiveView { // If this session is active and there is no active view, update `CrashContext` accordingly, so eventual crash // won't be associated to an inactive view and instead we will consider starting background view to track it. // We also want to send this as a session is being stopped. @@ -194,10 +195,6 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { dependencies.core.send(message: .custom(key: "rum", baggage: [RUMBaggageKeys.viewReset: true])) } - if deactivating { - isActive = false - } - return isActive || !viewScopes.isEmpty } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 5fde38201d..a747798c7e 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -169,7 +169,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { needsViewUpdate = true // Session stop - case let _ as RUMStopSessionCommand: + case is RUMStopSessionCommand: isActiveView = false needsViewUpdate = true diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift index d4bef6a722..1161968ca5 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift @@ -71,7 +71,12 @@ extension RUMSessionMatcher { let eventMatchers = try requests .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody, eventsPatch: eventsPatch) } .filter { event in try event.eventType() != "telemetry" } - let sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers) + var sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers) + sessionMatchers.sort { + let a = $0.viewVisits.first?.viewEvents.first?.date ?? 0 + let b = $1.viewVisits.first?.viewEvents.first?.date ?? 0 + return a < b + } if sessionMatchers.count > maxCount { throw Exception( diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift new file mode 100644 index 0000000000..ff83cffeb3 --- /dev/null +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMStopSessionScenarioTests.swift @@ -0,0 +1,137 @@ +// 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 2023-Present Datadog, Inc. + +import Foundation +import XCTest + +class RUMStopSessionKioskScreen: XCUIApplication { + func tapStartSession() { + buttons["Start Session"].tap() + } + + func tapStartInterruptedSession() { + buttons["Start Interrupted Session"].tap() + } +} + +class RUMSendKioskEventsScreen: XCUIApplication { + func back() { + navigationBars.buttons["Back"].tap() + } + + func tapDownloadResourceAndWait() { + buttons["Download Resource"].tap() + _ = buttons["Done"].waitForExistence(timeout: 2) + } +} + +class RUMSendKioskEventsInterruptedScreen: XCUIApplication { + func back() { + navigationBars.buttons["Back"].tap() + } + + func tapDownloadResource() { + buttons["Download Resource"].tap() + } +} + +class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { + func testRUMStopSessionScenario() throws { + // Server session recording RUM events send to `HTTPServerMock`. + let rumServerSession = server.obtainUniqueRecordingSession() + + let app = ExampleApplication() + app.launchWith( + testScenarioClassName: "RUMStopSessionsScenario", + serverConfiguration: HTTPServerMockConfiguration( + rumEndpoint: rumServerSession.recordingURL + ) + ) + + let kioskScreen = RUMStopSessionKioskScreen() + kioskScreen.tapStartSession() + let sendEventsScreen = RUMSendKioskEventsScreen() + sendEventsScreen.tapDownloadResourceAndWait() + sendEventsScreen.back() + + kioskScreen.tapStartInterruptedSession() + let sendInterruptedEventsScreen = RUMSendKioskEventsInterruptedScreen() + sendInterruptedEventsScreen.tapDownloadResource() + sendInterruptedEventsScreen.back() + + try app.endRUMSession() + + // Get RUM Sessions with expected number of View visits + let recordedRUMRequests = try rumServerSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + // 4th session should only contain the test session ending + let sessions = try RUMSessionMatcher.sessions(maxCount: 4, from: requests) + // No active views in any session + return sessions.count == 4 && sessions.allSatisfy { session in + !session.viewVisits.contains(where: { $0.viewEvents.last?.view.isActive == true }) + } + } + + assertRUM(requests: recordedRUMRequests) + + let sessions = try RUMSessionMatcher.sessions(maxCount: 4, from: recordedRUMRequests) + do { + let appStartSession = sessions[0] + + let launchView = try XCTUnwrap(appStartSession.applicationLaunchView) + XCTAssertEqual(launchView.actionEvents[0].action.type, .applicationStart) + + let view1 = appStartSession.viewVisits[0] + XCTAssertEqual(view1.name, "KioskViewController") + XCTAssertEqual(view1.path, "Example.KioskViewController") + XCTAssertEqual(view1.viewEvents.last?.session.isActive, false) + RUMSessionMatcher.assertViewWasEventuallyInactive(view1) + } + + // Second session sends a resource and ends returning to the KioskViewController + do { + let normalSession = sessions[1] + XCTAssertNil(normalSession.applicationLaunchView) + + let view1 = normalSession.viewVisits[0] + XCTAssertTrue(try XCTUnwrap(view1.viewEvents.first?.session.isActive)) + XCTAssertEqual(view1.name, "KioskSendEvents") + XCTAssertEqual(view1.path, "Example.KioskSendEventsViewController") + XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") + XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) + XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) + XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration, 100_000_000 - 1) // ~0.1s + XCTAssertLessThan(view1.resourceEvents[0].resource.duration, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) + RUMSessionMatcher.assertViewWasEventuallyInactive(view1) + + let view2 = normalSession.viewVisits[1] + XCTAssertEqual(view2.name, "KioskViewController") + XCTAssertEqual(view2.path, "Example.KioskViewController") + XCTAssertEqual(view2.viewEvents.last?.session.isActive, false) + RUMSessionMatcher.assertViewWasEventuallyInactive(view2) + } + + // Third session, same as the first but longer before completing resources + do { + let interruptedSession = sessions[2] + XCTAssertNil(interruptedSession.applicationLaunchView) + + let view1 = interruptedSession.viewVisits[0] + XCTAssertTrue(try XCTUnwrap(view1.viewEvents.first?.session.isActive)) + XCTAssertEqual(view1.name, "KioskSendInterruptedEvents") + XCTAssertEqual(view1.path, "Example.KioskSendInterruptedEventsViewController") + XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") + XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) + XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) + XCTAssertGreaterThan(view1.resourceEvents[0].resource.duration, 100_000_000 - 1) // ~0.1s + XCTAssertLessThan(view1.resourceEvents[0].resource.duration, 1_000_000_000 * 30) // less than 30s (big enough to balance NTP sync) + RUMSessionMatcher.assertViewWasEventuallyInactive(view1) + + let view2 = interruptedSession.viewVisits[1] + XCTAssertEqual(view2.name, "KioskViewController") + XCTAssertEqual(view2.path, "Example.KioskViewController") + XCTAssertEqual(view2.viewEvents.last?.session.isActive, false) + RUMSessionMatcher.assertViewWasEventuallyInactive(view2) + } + } +} From 076b87ad5459ba58ac8b6fda5fe9aaf1d3a14066 Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Wed, 22 Mar 2023 15:26:15 -0400 Subject: [PATCH 36/72] RUMM-2872 Restart last known view on a new session --- .../Scopes/RUMApplicationScope.swift | 23 ++++++--- .../RUMMonitor/Scopes/RUMSessionScope.swift | 21 +++++++- .../Scopes/RUMApplicationScopeTests.swift | 51 +++++++++++++++++-- .../Scopes/RUMSessionScopeTests.swift | 2 - 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 7b69d7fa65..7091648a3a 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -17,6 +17,9 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { /// Might be re-created later according to session duration constraints. private(set) var sessionScopes: [RUMSessionScope] = [] + /// Last active view from the last active session. Used to restart the active view on a user action. + private var lastActiveView: RUMViewScope? + var activeSession: RUMSessionScope? { get { return sessionScopes.first(where: { $0.isActive }) } } @@ -51,7 +54,12 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { if activeSession == nil && command.isUserInteraction { // No active sessions, start a new one - startSession(on: command, context: context, writer: writer) + startNewSession(on: command, context: context, writer: writer) + } + + if command is RUMStopSessionCommand { + // Reach in and grab the last active view + lastActiveView = activeSession?.viewScopes.first(where: { $0.isActiveView }) } // Can't use scope(byPropagating:context:writer) because of the extra step in looking for sessions @@ -112,17 +120,20 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { } } - private func startSession(on command: RUMCommand, context: DatadogContext, writer: Writer) { - let session = RUMSessionScope( + private func startNewSession(on command: RUMCommand, context: DatadogContext, writer: Writer) { + let resumingViewScope = command is RUMStartViewCommand ? nil : lastActiveView + let newSession = RUMSessionScope( isInitialSession: false, parent: self, startTime: command.time, dependencies: dependencies, - isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded + isReplayBeingRecorded: context.srBaggage?.isReplayBeingRecorded, + resumingViewScope: resumingViewScope ) + lastActiveView = nil - sessionScopes.append(session) - sessionScopeDidUpdate(session) + sessionScopes.append(newSession) + sessionScopeDidUpdate(newSession) } private func sessionScopeDidUpdate(_ sessionScope: RUMSessionScope) { diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index a4de8e6487..9c22e616c4 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -63,7 +63,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { parent: RUMContextProvider, startTime: Date, dependencies: RUMScopeDependencies, - isReplayBeingRecorded: Bool? + isReplayBeingRecorded: Bool?, + resumingViewScope: RUMViewScope? = nil ) { self.parent = parent self.dependencies = dependencies @@ -81,6 +82,24 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { didStartWithReplay: isReplayBeingRecorded ) + if let viewScope = resumingViewScope, + let viewIdentifiable = viewScope.identity.identifiable { + viewScopes.append( + RUMViewScope( + isInitialView: false, + parent: self, + dependencies: dependencies, + identity: viewIdentifiable, + path: viewScope.viewPath, + name: viewScope.viewName, + attributes: viewScope.attributes, + customTimings: [:], + startTime: startTime, + serverTimeOffset: viewScope.serverTimeOffset + ) + ) + } + // Update `CrashContext` with recent RUM session state: dependencies.core.send(message: .custom(key: "rum", baggage: [RUMBaggageKeys.sessionState: state])) } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index be0aa4e4d9..cea3b61e60 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -186,7 +186,7 @@ class RUMApplicationScopeTests: XCTestCase { func testWhenStoppingSession_itHasNoActiveSesssion() throws { // Given - var currentTime = Date() + let currentTime = Date() let scope = RUMApplicationScope( dependencies: .mockWith( sessionSampler: .mockKeepAll() @@ -206,7 +206,7 @@ class RUMApplicationScopeTests: XCTestCase { func testGivenStoppedSession_whenUserActionEvent_itStartsANewSession() throws { // Given - var currentTime = Date() + let currentTime = Date() let scope = RUMApplicationScope( dependencies: .mockWith( sessionSampler: .mockKeepAll() @@ -233,9 +233,49 @@ class RUMApplicationScopeTests: XCTestCase { XCTAssertNotNil(scope.activeSession) } + func testGivenStoppedSession_whenAUserActionOccurs_itRestartsTheLastKnownView() throws { + // Given + let currentTime = Date() + let scope = RUMApplicationScope( + dependencies: .mockWith( + sessionSampler: .mockKeepAll() + ) + ) + let viewName: String = .mockRandom() + let viewPath: String = .mockRandom() + XCTAssertTrue(scope.process( + command: RUMStartViewCommand.mockWith( + name: viewName, + path: viewPath + ), + context: context, + writer: writer + )) + XCTAssertFalse(scope.process( + command: RUMStopSessionCommand.mockWith(time: currentTime.addingTimeInterval(2)), + context: context, + writer: writer + )) + let secondSesionStartTime = currentTime.addingTimeInterval(3) + XCTAssertTrue(scope.process( + command: RUMCommandMock(time: secondSesionStartTime, isUserInteraction: true), + context: context, + writer: writer + )) + + // Then + XCTAssertEqual(scope.sessionScopes.count, 1) + let activeSession = try XCTUnwrap(scope.activeSession) + XCTAssertEqual(activeSession.viewScopes.count, 1) + let activeView = try XCTUnwrap(activeSession.viewScopes.first) + XCTAssertEqual(activeView.viewPath, viewPath) + XCTAssertEqual(activeView.viewName, viewName) + XCTAssertEqual(activeView.viewStartTime, secondSesionStartTime) + } + func testGivenStoppedSession_whenNonUserIntaractionEvent_itDoesNotStartANewSession() throws { // Given - var currentTime = Date() + let currentTime = Date() let scope = RUMApplicationScope( dependencies: .mockWith( sessionSampler: .mockKeepAll() @@ -264,7 +304,7 @@ class RUMApplicationScopeTests: XCTestCase { func testGivenStoppedSessionProcessingResources_itCanStayInactive() throws { // Given - var currentTime = Date() + let currentTime = Date() let scope = RUMApplicationScope( dependencies: .mockWith( sessionSampler: .mockKeepAll() @@ -288,7 +328,7 @@ class RUMApplicationScopeTests: XCTestCase { func testGivenStoppedSessionProcessingResources_itIsRemovedWhenFinished() throws { // Given - var currentTime = Date() + let currentTime = Date() let scope = RUMApplicationScope( dependencies: .mockWith( sessionSampler: .mockKeepAll() @@ -326,6 +366,7 @@ class RUMApplicationScopeTests: XCTestCase { // Then XCTAssertEqual(scope.sessionScopes.count, 1) + XCTAssertNotEqual(scope.activeSession?.sessionUUID, firstSession?.sessionUUID) XCTAssertEqual(scope.activeSession?.sessionUUID, secondSession?.sessionUUID) } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 9c9459c74c..6a637f6209 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -574,11 +574,9 @@ class RUMSessionScopeTests: XCTestCase { func testWhenScopeEnded_itDoesNotResetContextNextUpdate() { // Given var viewResetCallCount = 0 - var viewEvent: RUMViewEvent? = nil let messageReciever = FeatureMessageReceiverMock { message in if case let .custom(_, baggage) = message, baggage[RUMBaggageKeys.viewReset, type: Bool.self] == true { viewResetCallCount += 1 - viewEvent = nil } } From baf4efc1be987dd0b6811ce6c454ba25e533314e Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Wed, 22 Mar 2023 19:13:19 +0100 Subject: [PATCH 37/72] RUMM-3107 Silence unused warning --- Sources/_Datadog_Private/ObjcAppLaunchHandler.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/_Datadog_Private/ObjcAppLaunchHandler.m b/Sources/_Datadog_Private/ObjcAppLaunchHandler.m index 86b236e7ed..acaafbc442 100644 --- a/Sources/_Datadog_Private/ObjcAppLaunchHandler.m +++ b/Sources/_Datadog_Private/ObjcAppLaunchHandler.m @@ -41,10 +41,10 @@ + (void)load { loadTime:CFAbsoluteTimeGetCurrent()]; NSNotificationCenter * __weak center = NSNotificationCenter.defaultCenter; - id __block token = [center addObserverForName:UIApplicationDidBecomeActiveNotification - object:nil - queue:NSOperationQueue.mainQueue - usingBlock:^(NSNotification *_){ + id __block __unused token = [center addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *_){ @synchronized(_shared) { NSTimeInterval time = CFAbsoluteTimeGetCurrent() - _shared->_processStartTime; From d55fb903e3db542f1ec2e53aed0468f43ae1809b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 23 Mar 2023 13:53:30 +0000 Subject: [PATCH 38/72] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061969035c..a2d879ed00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +# 1.17.0 / 23-03-2023 +- [BUGFIX] Fix crash in `VitalInfoSampler`. See [#1216][] (Thanks [@cltnschlosser][]) + # 1.16.0 / 02-03-2023 - [IMPROVEMENT] Always create an ApplicationLaunch view on session initialization. See [#1160][] - [BUGFIX] Remove the data race caused by sampling on the RUM thread. See [#1177][] (Thanks [@cltnschlosser][]) @@ -438,6 +441,7 @@ [#1160]: https://github.com/DataDog/dd-sdk-ios/pull/1160 [#1177]: https://github.com/DataDog/dd-sdk-ios/pull/1177 [#1188]: https://github.com/DataDog/dd-sdk-ios/pull/1188 +[#1216]: https://github.com/DataDog/dd-sdk-ios/pull/1216 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu From be472158d00274fe3d6dc9bb941ddcf86425e266 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 23 Mar 2023 13:53:40 +0000 Subject: [PATCH 39/72] Bumped version to 1.17.0 --- DatadogSDK.podspec | 2 +- DatadogSDKAlamofireExtension.podspec | 2 +- DatadogSDKCrashReporting.podspec | 4 ++-- DatadogSDKObjc.podspec | 4 ++-- DatadogSDKSessionReplay.podspec | 2 +- Sources/Datadog/Versioning.swift | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DatadogSDK.podspec b/DatadogSDK.podspec index 20f71ae33f..9309bafb77 100644 --- a/DatadogSDK.podspec +++ b/DatadogSDK.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDK" s.module_name = "Datadog" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKAlamofireExtension.podspec b/DatadogSDKAlamofireExtension.podspec index 620cd2435b..f03adb14e0 100644 --- a/DatadogSDKAlamofireExtension.podspec +++ b/DatadogSDKAlamofireExtension.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKAlamofireExtension" s.module_name = "DatadogAlamofireExtension" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKCrashReporting.podspec b/DatadogSDKCrashReporting.podspec index 83503f1b0d..6d9ec7f0c4 100644 --- a/DatadogSDKCrashReporting.podspec +++ b/DatadogSDKCrashReporting.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKCrashReporting" s.module_name = "DatadogCrashReporting" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" @@ -22,6 +22,6 @@ Pod::Spec.new do |s| s.static_framework = true s.source_files = "Sources/DatadogCrashReporting/**/*.swift" - s.dependency 'DatadogSDK', '1.16.0' + s.dependency 'DatadogSDK', '1.17.0' s.dependency 'PLCrashReporter', '~> 1.11.0' end diff --git a/DatadogSDKObjc.podspec b/DatadogSDKObjc.podspec index e50e11db23..8751a28a7b 100644 --- a/DatadogSDKObjc.podspec +++ b/DatadogSDKObjc.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKObjc" s.module_name = "DatadogObjc" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" @@ -21,5 +21,5 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => s.version.to_s } s.source_files = "Sources/DatadogObjc/**/*.swift" - s.dependency 'DatadogSDK', '1.16.0' + s.dependency 'DatadogSDK', '1.17.0' end diff --git a/DatadogSDKSessionReplay.podspec b/DatadogSDKSessionReplay.podspec index 4bd37f0385..99c958e0f2 100644 --- a/DatadogSDKSessionReplay.podspec +++ b/DatadogSDKSessionReplay.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKSessionReplay" s.module_name = "DatadogSessionReplay" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Official Datadog Session Replay SDK for iOS. This module is currently in beta - contact Datadog to request a try." s.homepage = "https://www.datadoghq.com" diff --git a/Sources/Datadog/Versioning.swift b/Sources/Datadog/Versioning.swift index 0fdcfd15df..c1017bf803 100644 --- a/Sources/Datadog/Versioning.swift +++ b/Sources/Datadog/Versioning.swift @@ -1,3 +1,3 @@ // GENERATED FILE: Do not edit directly -internal let __sdkVersion = "1.16.0" +internal let __sdkVersion = "1.17.0" From b9102f7381bd3150106c401193fcd305763861dc Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 23 Mar 2023 14:04:34 +0000 Subject: [PATCH 40/72] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d879ed00..adc2984a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 1.17.0 / 23-03-2023 - [BUGFIX] Fix crash in `VitalInfoSampler`. See [#1216][] (Thanks [@cltnschlosser][]) +- [IMPROVEMENT] Fix Xcode analysis warning. See[#1220][] # 1.16.0 / 02-03-2023 - [IMPROVEMENT] Always create an ApplicationLaunch view on session initialization. See [#1160][] @@ -442,6 +443,7 @@ [#1177]: https://github.com/DataDog/dd-sdk-ios/pull/1177 [#1188]: https://github.com/DataDog/dd-sdk-ios/pull/1188 [#1216]: https://github.com/DataDog/dd-sdk-ios/pull/1216 +[#1220]: https://github.com/DataDog/dd-sdk-ios/pull/1220 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu From 046151a03c6499534c175450440a28118e5b8bbd Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Thu, 23 Mar 2023 10:27:29 -0400 Subject: [PATCH 41/72] Update Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift Co-authored-by: Maciej Burda --- .../Scenarios/RUM/RUMCommonAsserts.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift index 1161968ca5..5699f5ad34 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift @@ -71,12 +71,9 @@ extension RUMSessionMatcher { let eventMatchers = try requests .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody, eventsPatch: eventsPatch) } .filter { event in try event.eventType() != "telemetry" } - var sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers) - sessionMatchers.sort { - let a = $0.viewVisits.first?.viewEvents.first?.date ?? 0 - let b = $1.viewVisits.first?.viewEvents.first?.date ?? 0 - return a < b - } + let sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers).sorted(by: { + return $0.viewVisits.first?.viewEvents.first?.date ?? 0 < $1.viewVisits.first?.viewEvents.first?.date ?? 0 + }) if sessionMatchers.count > maxCount { throw Exception( From b61526a4fcc5063040b7ffb8c44eb8fd8ac7d5d7 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 09:32:24 +0000 Subject: [PATCH 42/72] REPLAY-1347 PR fixes --- .../Utilities/ImageDataProvider.swift | 14 +++++++++---- .../ViewTreeSnapshotBuilder.swift | 8 +++++--- .../Tests/Mocks/MockImageDataProvider.swift | 20 ++++++++++++++++--- .../Tests/Mocks/RecorderMocks.swift | 9 ++++++--- .../Utilties/ImageDataProviderTests.swift | 10 ++++++++++ .../UIImageViewWireframesBuilderTests.swift | 2 +- .../ViewTreeSnapshotBuilderTests.swift | 9 ++++++--- 7 files changed, 55 insertions(+), 17 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index f83ba9c139..a20bfece75 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -18,13 +18,17 @@ internal protocol ImageDataProviding { ) -> String } -internal class ImageDataProvider: ImageDataProviding { +final internal class ImageDataProvider: ImageDataProviding { private var cache: Cache + private let maxBytesSize: Int + internal init( - cache: Cache = .init() + cache: Cache = .init(), + maxBytesSize: Int = 10_000 ) { self.cache = cache + self.maxBytesSize = maxBytesSize } func contentBase64String( @@ -45,10 +49,12 @@ internal class ImageDataProvider: ImageDataProviding { } if let base64EncodedImage = cache[identifier] { return base64EncodedImage - } else { - let base64EncodedImage = image.pngData()?.base64EncodedString() ?? "" + } else if let base64EncodedImage = image.pngData()?.base64EncodedString(), base64EncodedImage.count <= maxBytesSize { cache[identifier, base64EncodedImage.count] = base64EncodedImage return base64EncodedImage + } else { + cache[identifier] = "" + return "" } } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 849845d43d..0880801ced 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -20,7 +20,7 @@ internal struct ViewTreeSnapshotBuilder { /// Text obfuscator applied to all sensitive texts. let sensitiveTextObfuscator = SensitiveTextObfuscator() /// Provides base64 image data with a built in caching mechanism. - let imageDataProvider: ImageDataProvider + let imageDataProvider: ImageDataProviding /// Builds the `ViewTreeSnapshot` for given root view. /// @@ -46,7 +46,8 @@ internal struct ViewTreeSnapshotBuilder { case .allowAll: return nopTextObfuscator } }(), - sensitiveTextObfuscator: sensitiveTextObfuscator + sensitiveTextObfuscator: sensitiveTextObfuscator, + imageDataProvider: imageDataProvider ) let snapshot = ViewTreeSnapshot( date: recorderContext.date.addingTimeInterval(recorderContext.rumContext.viewServerTimeOffset ?? 0), @@ -62,7 +63,8 @@ extension ViewTreeSnapshotBuilder { init() { self.init( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()), - idsGenerator: NodeIDGenerator() + idsGenerator: NodeIDGenerator(), + imageDataProvider: ImageDataProvider() ) } } diff --git a/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift b/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift index 9cbc1d2a0f..14ce4cb1ac 100644 --- a/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift +++ b/DatadogSessionReplay/Tests/Mocks/MockImageDataProvider.swift @@ -7,8 +7,22 @@ import UIKit @testable import DatadogSessionReplay -class MockImageDataProvider: ImageDataProvider { - override func contentBase64String(of image: UIImage?, tintColor: UIColor?) -> String { - return "mock_base64_string" +struct MockImageDataProvider: ImageDataProviding { + var contentBase64String: String + + func contentBase64String(of image: UIImage?) -> String { + return contentBase64String + } + + func contentBase64String(of image: UIImage?, tintColor: UIColor?) -> String { + return contentBase64String } + + init(contentBase64String: String = "mock_base64_string") { + self.contentBase64String = contentBase64String + } +} + +internal func mockRandomImageDataProvider() -> ImageDataProviding { + return MockImageDataProvider(contentBase64String: .mockRandom()) } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index fe1931f8a1..fbf0929e88 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -291,7 +291,8 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { ids: NodeIDGenerator(), textObfuscator: mockRandomTextObfuscator(), selectionTextObfuscator: mockRandomTextObfuscator(), - sensitiveTextObfuscator: mockRandomTextObfuscator() + sensitiveTextObfuscator: mockRandomTextObfuscator(), + imageDataProvider: mockRandomImageDataProvider() ) } @@ -301,7 +302,8 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { ids: NodeIDGenerator = NodeIDGenerator(), textObfuscator: TextObfuscating = NOPTextObfuscator(), selectionTextObfuscator: TextObfuscating = NOPTextObfuscator(), - sensitiveTextObfuscator: TextObfuscating = NOPTextObfuscator() + sensitiveTextObfuscator: TextObfuscating = NOPTextObfuscator(), + imageDataProvider: ImageDataProviding = MockImageDataProvider() ) -> ViewTreeRecordingContext { return .init( recorder: recorder, @@ -309,7 +311,8 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { ids: ids, textObfuscator: textObfuscator, selectionTextObfuscator: selectionTextObfuscator, - sensitiveTextObfuscator: sensitiveTextObfuscator + sensitiveTextObfuscator: sensitiveTextObfuscator, + imageDataProvider: imageDataProvider ) } } diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift index d7f5d241bc..a36ab3e549 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift @@ -41,6 +41,16 @@ class ImageDataProviderTests: XCTestCase { XCTAssertGreaterThan(imageData.count, 0) } + func test_ignoresAboveSize() throws { + let sut = ImageDataProvider( + maxBytesSize: 1 + ) + let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil) + + let imageData = try XCTUnwrap(sut.contentBase64String(of: image)) + XCTAssertEqual(imageData.count, 0) + } + func test_imageIdentifierConsistency() { var ids = Set() for _ in 0..<100 { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index 761ea3036a..6388b5acfb 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -58,7 +58,7 @@ class UIImageViewWireframesBuilderTests: XCTestCase { contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), clipsToBounds: true, image: UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil), - imageDataProvider: MockImageDataProvider(), + imageDataProvider: mockRandomImageDataProvider(), tintColor: UIColor.mockRandom(), shouldRecordImage: false ) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift index 176c544e5b..c6bcca6727 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilderTests.swift @@ -16,7 +16,8 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), - idsGenerator: NodeIDGenerator() + idsGenerator: NodeIDGenerator(), + imageDataProvider: MockImageDataProvider() ) // When @@ -36,7 +37,8 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), - idsGenerator: NodeIDGenerator() + idsGenerator: NodeIDGenerator(), + imageDataProvider: MockImageDataProvider() ) // When @@ -63,7 +65,8 @@ class ViewTreeSnapshotBuilderTests: XCTestCase { let nodeRecorder = NodeRecorderMock(resultForView: { _ in nil }) let builder = ViewTreeSnapshotBuilder( viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]), - idsGenerator: NodeIDGenerator() + idsGenerator: NodeIDGenerator(), + imageDataProvider: MockImageDataProvider() ) // When From f7e7fe4b8d52092443eaf7aeff34456e66eabc8b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 09:36:44 +0000 Subject: [PATCH 43/72] REPLAY-1347 Linter fix --- .../Sources/Recorder/Utilities/ImageDataProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index a20bfece75..655c2cc167 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -18,7 +18,7 @@ internal protocol ImageDataProviding { ) -> String } -final internal class ImageDataProvider: ImageDataProviding { +internal final class ImageDataProvider: ImageDataProviding { private var cache: Cache private let maxBytesSize: Int From b6ca7ab791c42b1f8862a35431c384dfed8924c3 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Fri, 24 Mar 2023 16:35:11 +0100 Subject: [PATCH 44/72] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc2984a6b..749ca0d51a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ # 1.17.0 / 23-03-2023 - [BUGFIX] Fix crash in `VitalInfoSampler`. See [#1216][] (Thanks [@cltnschlosser][]) -- [IMPROVEMENT] Fix Xcode analysis warning. See[#1220][] +- [IMPROVEMENT] Fix Xcode analysis warning. See [#1220][] +- [BUGFIX] Send crashes to both RUM and Logs. See [#1209][] # 1.16.0 / 02-03-2023 - [IMPROVEMENT] Always create an ApplicationLaunch view on session initialization. See [#1160][] @@ -442,6 +443,7 @@ [#1160]: https://github.com/DataDog/dd-sdk-ios/pull/1160 [#1177]: https://github.com/DataDog/dd-sdk-ios/pull/1177 [#1188]: https://github.com/DataDog/dd-sdk-ios/pull/1188 +[#1209]: https://github.com/DataDog/dd-sdk-ios/pull/1209 [#1216]: https://github.com/DataDog/dd-sdk-ios/pull/1216 [#1220]: https://github.com/DataDog/dd-sdk-ios/pull/1220 [@00fa9a]: https://github.com/00FA9A From bac90a0f1bc3325e2e14479506d771c1e91501b8 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 14:48:49 +0000 Subject: [PATCH 45/72] REPLAY-1459 Add convenience api for time and byte size --- .../Utilities/SwiftExtensionsTests.swift | 14 +++++++ .../Tests/Writer/SRCompressionTests.swift | 4 +- Sources/Datadog/Utils/SwiftExtensions.swift | 38 +++++++++++++++++++ .../Persistence/FilesOrchestratorTests.swift | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift b/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift index 04bd671182..06c4de2959 100644 --- a/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift @@ -44,6 +44,13 @@ class FixedWidthIntegerTests: XCTestCase { let expectedConvertedValue = Int.min XCTAssertEqual(expectedConvertedValue, convertedValue) } + + func testBytesSizeConvenience() { + let size = 1 + XCTAssertEqual(size.KB, size * 1_024) + XCTAssertEqual(size.MB, size * 1_024 * 1_024) + XCTAssertEqual(size.GB, size * 1_024 * 1_024 * 1_024) + } } class TimeIntervalTests: XCTestCase { @@ -65,4 +72,11 @@ class TimeIntervalTests: XCTestCase { let uInt64MaxDate = Date(timeIntervalSinceReferenceDate: -.greatestFiniteMagnitude) XCTAssertEqual(uInt64MaxDate.timeIntervalSince1970.toInt64Milliseconds, Int64.min) } + + func testTimeConvienience() { + let time = Date.mockDecember15th2019At10AMUTC().timeIntervalSince1970 + XCTAssertEqual(time.minutes, time * 60) + XCTAssertEqual(time.hours, time * 60 * 60) + XCTAssertEqual(time.days, time * 60 * 60 * 24) + } } diff --git a/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift b/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift index bb76b3fab9..87d5095823 100644 --- a/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift +++ b/DatadogSessionReplay/Tests/Writer/SRCompressionTests.swift @@ -89,7 +89,7 @@ struct Deflate { /// - data: The compressed data. /// - capacity: Capacity of the allocated memory to contain the decoded data. 1MB by default. /// - Returns: Decompressed data. - static func decode(_ data: Data, capacity: Int = 1_000_000) -> Data? { + static func decode(_ data: Data, capacity: Int = 1.MB) -> Data? { // Skip `deflate` header (2 bytes) and checksum (4 bytes) // validations and inflate raw deflated data. let range = 2.. Data? { + static func decompress(_ data: Data, capacity: Int = 1.MB) -> Data? { data.withUnsafeBytes { guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else { return nil diff --git a/Sources/Datadog/Utils/SwiftExtensions.swift b/Sources/Datadog/Utils/SwiftExtensions.swift index d1fbe0bb3f..d28af606a1 100644 --- a/Sources/Datadog/Utils/SwiftExtensions.swift +++ b/Sources/Datadog/Utils/SwiftExtensions.swift @@ -101,3 +101,41 @@ extension Array { 0 <= index && index < count ? self[index] : nil } } + +// MARK: - Time Convinience + +public extension TimeInterval { + var seconds: TimeInterval { return TimeInterval(self) } + var minutes: TimeInterval { return self.multiplyOrClamp(by: 60) } + var hours: TimeInterval { return self.multiplyOrClamp(by: 60.minutes) } + var days: TimeInterval { return self.multiplyOrClamp(by: 24.hours) } + + private func multiplyOrClamp(by factor: TimeInterval) -> TimeInterval { + guard factor != 0 else { + return 0 + } + + let multiplied = TimeInterval(self) * factor + if multiplied / factor != TimeInterval(self) { + return TimeInterval.greatestFiniteMagnitude + } + return multiplied + } +} + +public extension FixedWidthInteger { + var seconds: TimeInterval { return TimeInterval(self) } + var minutes: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: 60).partialValue) } + var hours: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: Self(60.minutes)).partialValue) } + var days: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: Self(24.hours)).partialValue) } +} + +// MARK: - Bytes Size Convenience + +public extension FixedWidthInteger { + private var base: Self { 1_024 } + var KB: Self { return self.multipliedReportingOverflow(by: base).partialValue } + var MB: Self { return self.KB.multipliedReportingOverflow(by: base).partialValue } + var GB: Self { return self.MB.multipliedReportingOverflow(by: base).partialValue } + var bytes: Self { return self } +} diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift index 9bd1c8171b..5d455a71f1 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -120,7 +120,7 @@ class FilesOrchestratorTests: XCTestCase { } func testWhenFilesDirectorySizeIsBig_itKeepsItUnderLimit_byRemovingOldestFilesFirst() throws { - let oneMB: UInt64 = 1_024 * 1_024 + let oneMB: UInt64 = 1.MB let orchestrator = FilesOrchestrator( directory: temporaryDirectory, From 36dfa6f030b3f9c9313c3391d0bddceba7d3e55b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 14:49:07 +0000 Subject: [PATCH 46/72] REPLAY-1459 Add performance preset override --- .../Sources/Drafts/SessionReplayFeature.swift | 6 ++- Sources/Datadog/Core/PerformancePreset.swift | 37 +++++++++++++++++-- Sources/Datadog/DatadogCore/DatadogCore.swift | 6 ++- .../DatadogInternal/DatadogFeature.swift | 3 ++ .../DatadogInternal/DatadogFeatureMocks.swift | 1 + 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift index 1ea6c58878..9047149d75 100644 --- a/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Drafts/SessionReplayFeature.swift @@ -19,6 +19,7 @@ internal class SessionReplayFeature: DatadogFeature, SessionReplayController { let name: String = "session-replay" let requestBuilder: FeatureRequestBuilder let messageReceiver: FeatureMessageReceiver + let performanceOverride: PerformancePresetOverride? // MARK: - Integrations with other features @@ -57,7 +58,10 @@ internal class SessionReplayFeature: DatadogFeature, SessionReplayController { self.writer = writer self.requestBuilder = RequestBuilder(customUploadURL: configuration.customUploadURL) self.contextPublisher = SRContextPublisher(core: core) - + self.performanceOverride = PerformancePresetOverride( + maxFileSize: UInt64(10).MB, + maxObjectSize: UInt64(10).MB + ) // Set initial SR context (it is configured, but not yet started): contextPublisher.setRecordingIsPending(false) } diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index 5fceca49bd..401bef27f8 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -113,16 +113,45 @@ internal extension PerformancePreset { minUploadDelay: TimeInterval, uploadDelayFactors: (initial: Double, default: Double, min: Double, max: Double, changeRate: Double) ) { - self.maxFileSize = 4 * 1_024 * 1_024 // 4MB - self.maxDirectorySize = 512 * 1_024 * 1_024 // 512 MB + self.maxFileSize = UInt64(4).MB + self.maxDirectorySize = UInt64(512).MB self.maxFileAgeForWrite = meanFileAge * 0.95 // 5% below the mean age self.minFileAgeForRead = meanFileAge * 1.05 // 5% above the mean age - self.maxFileAgeForRead = 18 * 60 * 60 // 18h + self.maxFileAgeForRead = 18.hours self.maxObjectsInFile = 500 - self.maxObjectSize = 512 * 1_024 // 512KB + self.maxObjectSize = UInt64(512).KB self.initialUploadDelay = minUploadDelay * uploadDelayFactors.initial self.minUploadDelay = minUploadDelay * uploadDelayFactors.min self.maxUploadDelay = minUploadDelay * uploadDelayFactors.max self.uploadDelayChangeRate = uploadDelayFactors.changeRate } + + func updated(with: PerformancePresetOverride?) -> PerformancePreset { + guard let with = with else { + return self + } + return PerformancePreset( + maxFileSize: with.maxFileSize, + maxDirectorySize: maxDirectorySize, + maxFileAgeForWrite: maxFileAgeForWrite, + minFileAgeForRead: minFileAgeForRead, + maxFileAgeForRead: maxFileAgeForRead, + maxObjectsInFile: maxObjectsInFile, + maxObjectSize: with.maxObjectSize, + initialUploadDelay: initialUploadDelay, + minUploadDelay: minUploadDelay, + maxUploadDelay: maxUploadDelay, + uploadDelayChangeRate: uploadDelayChangeRate + ) + } +} + +public struct PerformancePresetOverride { + let maxFileSize: UInt64 + let maxObjectSize: UInt64 + + public init(maxFileSize: UInt64, maxObjectSize: UInt64) { + self.maxFileSize = maxFileSize + self.maxObjectSize = maxObjectSize + } } diff --git a/Sources/Datadog/DatadogCore/DatadogCore.swift b/Sources/Datadog/DatadogCore/DatadogCore.swift index 768d7b1f4d..bc8c5320e5 100644 --- a/Sources/Datadog/DatadogCore/DatadogCore.swift +++ b/Sources/Datadog/DatadogCore/DatadogCore.swift @@ -277,12 +277,14 @@ extension DatadogCore: DatadogCoreProtocol { /* public */ func register(feature: DatadogFeature) throws { let featureDirectories = try directory.getFeatureDirectories(forFeatureNamed: feature.name) + let updatedPerformance = performance.updated(with: feature.performanceOverride) + let storage = FeatureStorage( featureName: feature.name, queue: readWriteQueue, directories: featureDirectories, dateProvider: dateProvider, - performance: performance, + performance: updatedPerformance, encryption: encryption ) @@ -292,7 +294,7 @@ extension DatadogCore: DatadogCoreProtocol { fileReader: storage.reader, requestBuilder: feature.requestBuilder, httpClient: httpClient, - performance: performance + performance: updatedPerformance ) v2Features[feature.name] = ( diff --git a/Sources/Datadog/DatadogInternal/DatadogFeature.swift b/Sources/Datadog/DatadogInternal/DatadogFeature.swift index cf5b359db1..fd602f0fa4 100644 --- a/Sources/Datadog/DatadogInternal/DatadogFeature.swift +++ b/Sources/Datadog/DatadogInternal/DatadogFeature.swift @@ -34,6 +34,9 @@ public protocol DatadogFeature { /// The `FeatureMessageReceiver` defines an interface for Feature to receive any message /// from a bus that is shared between all Features registered in the core. var messageReceiver: FeatureMessageReceiver { get } + + /// (Optional) `PerformancePresetOverride` allows overriding certain performance presets if needed. + var performanceOverride: PerformancePresetOverride? { get } } public protocol DatadogFeatureIntegration { diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogFeatureMocks.swift index 4ceba9b3b5..f98d35dff2 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogFeatureMocks.swift @@ -13,4 +13,5 @@ internal struct DatadogFeatureMock: DatadogFeature { var name: String = DatadogFeatureMock.featureName var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() var messageReceiver: FeatureMessageReceiver = FeatureMessageReceiverMock() + var performanceOverride: PerformancePresetOverride? = nil } From 05547e330c17e776b51de82445871c73f99601f2 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 14:49:16 +0000 Subject: [PATCH 47/72] REPLAY-1459 Add tests --- .../Datadog/Core/PerformancePresetTests.swift | 10 ++++++++++ .../Datadog/DatadogCore/DatadogCoreTests.swift | 1 + 2 files changed, 11 insertions(+) diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index f1bb2083e9..f6869d59f5 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -137,4 +137,14 @@ class PerformancePresetTests: XCTestCase { ) } } + + func testPresetsUpdate() { + let preset = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .mockRandom(), bundleType: .mockRandom()) + let updatedPreset = preset.updated(with: PerformancePresetOverride(maxFileSize: 0, maxObjectSize: 0)) + XCTAssertNotEqual(preset.maxFileSize, updatedPreset.maxFileSize) + XCTAssertNotEqual(preset.maxObjectSize, updatedPreset.maxObjectSize) + + let notUpdatedPreset = preset.updated(with: nil) + XCTAssertEqual(preset, notUpdatedPreset) + } } diff --git a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift index 5dcb82410d..1a2988da57 100644 --- a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -15,6 +15,7 @@ private struct FeatureMock: DatadogFeature { var name: String var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() var messageReceiver: FeatureMessageReceiver = FeatureMessageReceiverMock() + var performanceOverride: PerformancePresetOverride? = nil } class DatadogCoreTests: XCTestCase { From d883fdb1ff5784b58830808b0c110f313362e855 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 14:56:21 +0000 Subject: [PATCH 48/72] REPLAY-1459 PR fixes --- Sources/Datadog/Core/PerformancePreset.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index 401bef27f8..88fb73b964 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -127,17 +127,14 @@ internal extension PerformancePreset { } func updated(with: PerformancePresetOverride?) -> PerformancePreset { - guard let with = with else { - return self - } return PerformancePreset( - maxFileSize: with.maxFileSize, + maxFileSize: with?.maxFileSize ?? maxFileSize, maxDirectorySize: maxDirectorySize, maxFileAgeForWrite: maxFileAgeForWrite, minFileAgeForRead: minFileAgeForRead, maxFileAgeForRead: maxFileAgeForRead, maxObjectsInFile: maxObjectsInFile, - maxObjectSize: with.maxObjectSize, + maxObjectSize: with?.maxObjectSize ?? maxObjectSize, initialUploadDelay: initialUploadDelay, minUploadDelay: minUploadDelay, maxUploadDelay: maxUploadDelay, @@ -147,10 +144,10 @@ internal extension PerformancePreset { } public struct PerformancePresetOverride { - let maxFileSize: UInt64 - let maxObjectSize: UInt64 + let maxFileSize: UInt64? + let maxObjectSize: UInt64? - public init(maxFileSize: UInt64, maxObjectSize: UInt64) { + public init(maxFileSize: UInt64?, maxObjectSize: UInt64?) { self.maxFileSize = maxFileSize self.maxObjectSize = maxObjectSize } From caf3da8c98e914c4b1778786c5e28e437e8bdc9e Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 24 Mar 2023 15:33:15 +0000 Subject: [PATCH 49/72] REPLAY-1459 Fix tests build --- .../Datadog/Core/Persistence/FilesOrchestratorTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift index 5d455a71f1..56b990b804 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -120,7 +120,7 @@ class FilesOrchestratorTests: XCTestCase { } func testWhenFilesDirectorySizeIsBig_itKeepsItUnderLimit_byRemovingOldestFilesFirst() throws { - let oneMB: UInt64 = 1.MB + let oneMB = UInt64(1).MB let orchestrator = FilesOrchestrator( directory: temporaryDirectory, From 64a770242ea1f45c7833a1baef452ed71c94dcc9 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 27 Mar 2023 13:46:44 +0100 Subject: [PATCH 50/72] REPLAY-1459 PR fixes --- Datadog/Datadog.xcodeproj/project.pbxproj | 38 ++++++++++ .../Utilities/SwiftExtensionsTests.swift | 14 ---- Sources/Datadog/Core/PerformancePreset.swift | 10 --- Sources/Datadog/DatadogCore/DatadogCore.swift | 13 +++- .../DatadogInternal/DatadogFeature.swift | 4 + .../FixedWidthInteger+Convinience.swift | 28 +++++++ .../Extensions/TimeInterval+Convinience.swift | 73 +++++++++++++++++++ .../PerformancePresetOverride.swift | 32 ++++++++ Sources/Datadog/Utils/SwiftExtensions.swift | 38 ---------- .../DatadogCore/DatadogCoreTests.swift | 46 ++++++++++++ .../FixedWidthInteger+ConvinienceTests.swift | 45 ++++++++++++ .../TimeInterval+ConvinienceTests.swift | 42 +++++++++++ 12 files changed, 317 insertions(+), 66 deletions(-) create mode 100644 Sources/Datadog/DatadogInternal/Extensions/FixedWidthInteger+Convinience.swift create mode 100644 Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift create mode 100644 Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift create mode 100644 Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift create mode 100644 Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index d5e943af67..a131eae3fc 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -536,6 +536,16 @@ A728ADAC2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */; }; A728ADB02934EB0900397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; + A736BA2E29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */; }; + A736BA2F29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */; }; + A736BA3129D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */; }; + A736BA3229D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */; }; + A736BA3429D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */; }; + A736BA3529D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */; }; + A736BA3D29D1B7FE00C00966 /* TimeInterval+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */; }; + A736BA3E29D1B80200C00966 /* TimeInterval+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */; }; + A736BA3F29D1B80A00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */; }; + A736BA4029D1B80D00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */; }; A7609F272940AB4B00020D85 /* FirstPartyHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7609F262940AB4B00020D85 /* FirstPartyHosts.swift */; }; A7609F282940AB4B00020D85 /* FirstPartyHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7609F262940AB4B00020D85 /* FirstPartyHosts.swift */; }; A762BDE429351A250058D8E7 /* FirstPartyHostsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A762BDE329351A250058D8E7 /* FirstPartyHostsTests.swift */; }; @@ -1860,6 +1870,11 @@ A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersReaderTests.swift; sourceTree = ""; }; A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "W3CHTTPHeadersWriter+objc.swift"; sourceTree = ""; }; A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDW3CHTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; + A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePresetOverride.swift; sourceTree = ""; }; + A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Convinience.swift"; sourceTree = ""; }; + A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convinience.swift"; sourceTree = ""; }; + A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ConvinienceTests.swift"; sourceTree = ""; }; + A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+ConvinienceTests.swift"; sourceTree = ""; }; A7609F262940AB4B00020D85 /* FirstPartyHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstPartyHosts.swift; sourceTree = ""; }; A762BDE329351A250058D8E7 /* FirstPartyHostsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstPartyHostsTests.swift; sourceTree = ""; }; A79B0F5A292B7C06008742B3 /* OTelHTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTelHTTPHeadersWriterTests.swift; sourceTree = ""; }; @@ -4160,6 +4175,15 @@ path = W3C; sourceTree = ""; }; + A736BA3629D1B7AC00C00966 /* Extensions */ = { + isa = PBXGroup; + children = ( + A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */, + A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; A79B0F59292B7BF5008742B3 /* Propagation */ = { isa = PBXGroup; children = ( @@ -4242,6 +4266,8 @@ children = ( D21C26DF28AD2E47005DD405 /* DatadogExtended.swift */, D21C26E328AD30E2005DD405 /* Foundation+Datadog.swift */, + A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */, + A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */, ); path = Extensions; sourceTree = ""; @@ -4411,6 +4437,7 @@ D2956CAE2869D516007D5462 /* DatadogInternal */ = { isa = PBXGroup; children = ( + A736BA3629D1B7AC00C00966 /* Extensions */, D2CBC25C294215BE00134409 /* Codable */, D2956CAF2869D520007D5462 /* Context */, D29294E5291D65EA00F8EFF9 /* _InternalProxyTests.swift */, @@ -4489,6 +4516,7 @@ D2CBC2522942003B00134409 /* Codable */, 61E945DF2869BEF500A946C4 /* DD.swift */, D2D37DBE2846335F00FB4348 /* DatadogV1CoreProtocol.swift */, + A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */, ); path = DatadogInternal; sourceTree = ""; @@ -5568,6 +5596,7 @@ 6141015B251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift in Sources */, D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */, 616CCE16250A467E009FED46 /* RUMInstrumentation.swift in Sources */, + A736BA2E29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */, 6157FA5E252767CB009A8A3B /* URLSessionRUMResourcesHandler.swift in Sources */, 616CCE13250A1868009FED46 /* RUMCommandSubscriber.swift in Sources */, 6112B11425C84E7900B37771 /* CrashReportSender.swift in Sources */, @@ -5575,6 +5604,7 @@ 61133BE62423979B00786299 /* LogEventSanitizer.swift in Sources */, 615F197C25B5A64B00BE14B5 /* UIKitExtensions.swift in Sources */, 9EC8B5DA2668197B000F7529 /* VitalCPUReader.swift in Sources */, + A736BA3429D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */, 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */, 61B0385A2527247000518F3C /* DDURLSessionDelegate.swift in Sources */, 61133BDF2423979B00786299 /* SwiftExtensions.swift in Sources */, @@ -5691,6 +5721,7 @@ 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */, 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, 614B0A4B24EBC43D00A2A780 /* RUMUserInfoProvider.swift in Sources */, + A736BA3129D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */, D2A1EE23287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */, 61133BD22423979B00786299 /* Directory.swift in Sources */, ); @@ -5758,6 +5789,7 @@ 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */, D29294E6291D65EA00F8EFF9 /* _InternalProxyTests.swift in Sources */, + A736BA3F29D1B80A00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */, D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, 9EAF0CF6275A21100044E8CA /* WKUserContentController+DatadogTests.swift in Sources */, 61DA8CB2286215DE0074A606 /* CryptographyTests.swift in Sources */, @@ -5769,6 +5801,7 @@ 61F1A621249A45E400075390 /* DDSpanContextTests.swift in Sources */, 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */, E143CCAF27D236F600F4018A /* CITestIntegrationTests.swift in Sources */, + A736BA3D29D1B7FE00C00966 /* TimeInterval+ConvinienceTests.swift in Sources */, D234613228B7713000055D4C /* FeatureContextTests.swift in Sources */, D21C26EB28AFA11E005DD405 /* LogMessageReceiverTests.swift in Sources */, 615C3196251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift in Sources */, @@ -6275,6 +6308,7 @@ A7F773D52924EA2D00AC1A62 /* OTelHTTPHeaders.swift in Sources */, 6194E4BA28785BFD00EB6307 /* RemoteLogger.swift in Sources */, D2CB6E5C27C50EAE00A62B57 /* UIKitRUMUserActionsHandler.swift in Sources */, + A736BA2F29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */, D2CB6E5D27C50EAE00A62B57 /* RUMInstrumentation.swift in Sources */, D2CB6E5E27C50EAE00A62B57 /* URLSessionRUMResourcesHandler.swift in Sources */, D2CB6E5F27C50EAE00A62B57 /* RUMCommandSubscriber.swift in Sources */, @@ -6282,6 +6316,7 @@ D2CB6E6127C50EAE00A62B57 /* CrashReporter.swift in Sources */, D2CB6E6227C50EAE00A62B57 /* LogEventSanitizer.swift in Sources */, D20605B02874DAD70047275C /* CarrierInfo.swift in Sources */, + A736BA3529D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */, D2CB6E6327C50EAE00A62B57 /* UIKitExtensions.swift in Sources */, D2CB6E6427C50EAE00A62B57 /* VitalCPUReader.swift in Sources */, D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */, @@ -6398,6 +6433,7 @@ D2CB6EC627C50EAE00A62B57 /* HTTPHeadersWriter.swift in Sources */, D2CB6EC727C50EAE00A62B57 /* PerformancePreset.swift in Sources */, D2CB6EC827C50EAE00A62B57 /* RUMUserInfoProvider.swift in Sources */, + A736BA3229D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */, D21C26CF28A411FF005DD405 /* FeatureMessageReceiver.swift in Sources */, D2CB6EC927C50EAE00A62B57 /* Directory.swift in Sources */, ); @@ -6511,6 +6547,7 @@ D2CB6F2C27C520D400A62B57 /* JSONEncoderTests.swift in Sources */, D2CB6F2D27C520D400A62B57 /* LogFileOutputTests.swift in Sources */, D2CB6F2E27C520D400A62B57 /* RUMCommandTests.swift in Sources */, + A736BA4029D1B80D00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */, D2CB6F3027C520D400A62B57 /* DatadogExtensions.swift in Sources */, D2CB6F3227C520D400A62B57 /* JSONDataMatcher.swift in Sources */, D25085112976E30000E931C3 /* DatadogFeatureMocks.swift in Sources */, @@ -6560,6 +6597,7 @@ D2A717D02965DAB300EEC7D7 /* FeatureMessageReceiverTests.swift in Sources */, D2CB6F5927C520D400A62B57 /* WarningsTests.swift in Sources */, D2CB6F5A27C520D400A62B57 /* SwiftExtensionsTests.swift in Sources */, + A736BA3E29D1B80200C00966 /* TimeInterval+ConvinienceTests.swift in Sources */, 615950F0291C05CD00470E0C /* SessionReplayDependencyTests.swift in Sources */, D2CB6F5B27C520D400A62B57 /* RUMEventSanitizerTests.swift in Sources */, D2CB6F5D27C520D400A62B57 /* UIKitRUMViewsPredicateTests.swift in Sources */, diff --git a/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift b/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift index 06c4de2959..04bd671182 100644 --- a/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/SwiftExtensionsTests.swift @@ -44,13 +44,6 @@ class FixedWidthIntegerTests: XCTestCase { let expectedConvertedValue = Int.min XCTAssertEqual(expectedConvertedValue, convertedValue) } - - func testBytesSizeConvenience() { - let size = 1 - XCTAssertEqual(size.KB, size * 1_024) - XCTAssertEqual(size.MB, size * 1_024 * 1_024) - XCTAssertEqual(size.GB, size * 1_024 * 1_024 * 1_024) - } } class TimeIntervalTests: XCTestCase { @@ -72,11 +65,4 @@ class TimeIntervalTests: XCTestCase { let uInt64MaxDate = Date(timeIntervalSinceReferenceDate: -.greatestFiniteMagnitude) XCTAssertEqual(uInt64MaxDate.timeIntervalSince1970.toInt64Milliseconds, Int64.min) } - - func testTimeConvienience() { - let time = Date.mockDecember15th2019At10AMUTC().timeIntervalSince1970 - XCTAssertEqual(time.minutes, time * 60) - XCTAssertEqual(time.hours, time * 60 * 60) - XCTAssertEqual(time.days, time * 60 * 60 * 24) - } } diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index 88fb73b964..6648281746 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -142,13 +142,3 @@ internal extension PerformancePreset { ) } } - -public struct PerformancePresetOverride { - let maxFileSize: UInt64? - let maxObjectSize: UInt64? - - public init(maxFileSize: UInt64?, maxObjectSize: UInt64?) { - self.maxFileSize = maxFileSize - self.maxObjectSize = maxObjectSize - } -} diff --git a/Sources/Datadog/DatadogCore/DatadogCore.swift b/Sources/Datadog/DatadogCore/DatadogCore.swift index bc8c5320e5..3451b1a6bd 100644 --- a/Sources/Datadog/DatadogCore/DatadogCore.swift +++ b/Sources/Datadog/DatadogCore/DatadogCore.swift @@ -65,7 +65,7 @@ internal final class DatadogCore { /// Registry for Features. @ReadWriteLock - private var v2Features: [String: ( + private(set) var v2Features: [String: ( feature: DatadogFeature, storage: FeatureStorage, upload: FeatureUpload @@ -277,14 +277,19 @@ extension DatadogCore: DatadogCoreProtocol { /* public */ func register(feature: DatadogFeature) throws { let featureDirectories = try directory.getFeatureDirectories(forFeatureNamed: feature.name) - let updatedPerformance = performance.updated(with: feature.performanceOverride) + let performancePreset: PerformancePreset + if let override = feature.performanceOverride { + performancePreset = performance.updated(with: override) + } else { + performancePreset = performance + } let storage = FeatureStorage( featureName: feature.name, queue: readWriteQueue, directories: featureDirectories, dateProvider: dateProvider, - performance: updatedPerformance, + performance: performancePreset, encryption: encryption ) @@ -294,7 +299,7 @@ extension DatadogCore: DatadogCoreProtocol { fileReader: storage.reader, requestBuilder: feature.requestBuilder, httpClient: httpClient, - performance: updatedPerformance + performance: performancePreset ) v2Features[feature.name] = ( diff --git a/Sources/Datadog/DatadogInternal/DatadogFeature.swift b/Sources/Datadog/DatadogInternal/DatadogFeature.swift index fd602f0fa4..39a6d06c44 100644 --- a/Sources/Datadog/DatadogInternal/DatadogFeature.swift +++ b/Sources/Datadog/DatadogInternal/DatadogFeature.swift @@ -39,6 +39,10 @@ public protocol DatadogFeature { var performanceOverride: PerformancePresetOverride? { get } } +extension DatadogFeature { + public var performanceOverride: PerformancePresetOverride? { nil } +} + public protocol DatadogFeatureIntegration { /// The feature name. var name: String { get } diff --git a/Sources/Datadog/DatadogInternal/Extensions/FixedWidthInteger+Convinience.swift b/Sources/Datadog/DatadogInternal/Extensions/FixedWidthInteger+Convinience.swift new file mode 100644 index 0000000000..dad7d8c75d --- /dev/null +++ b/Sources/Datadog/DatadogInternal/Extensions/FixedWidthInteger+Convinience.swift @@ -0,0 +1,28 @@ +/* + * 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. + */ + +import Foundation + +/// An extension for FixedWidthInteger that provides a convenient API for +/// converting numeric values into different units of data storage, such as +/// bytes, kilobytes, megabytes, and gigabytes. +public extension FixedWidthInteger { + /// A private property that represents the base unit (1024) used for + /// converting between data storage units. + private var base: Self { 1_024 } + + /// A property that converts the given numeric value into kilobytes. + var KB: Self { return self.multipliedReportingOverflow(by: base).partialValue } + + /// A property that converts the given numeric value into megabytes. + var MB: Self { return self.KB.multipliedReportingOverflow(by: base).partialValue } + + /// A property that converts the given numeric value into gigabytes. + var GB: Self { return self.MB.multipliedReportingOverflow(by: base).partialValue } + + /// A helper property that returns the current value as a direct representation in bytes. + var bytes: Self { return self } +} diff --git a/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift b/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift new file mode 100644 index 0000000000..14c3ba95f4 --- /dev/null +++ b/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift @@ -0,0 +1,73 @@ +/* + * 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. + */ + +import Foundation + +/// An extension for TimeInterval that provides a more semantic and expressive +/// API for converting time representations into TimeInterval's default unit: seconds. +public extension TimeInterval { + /// A helper property that returns the current value as a direct representation in seconds. + var seconds: TimeInterval { return TimeInterval(self) } + + /// A property that converts the given number of minutes into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var minutes: TimeInterval { return self.multiplyOrClamp(by: 60) } + + /// A property that converts the given number of hours into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var hours: TimeInterval { return self.multiplyOrClamp(by: 60.minutes) } + + /// A property that converts the given number of days into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var days: TimeInterval { return self.multiplyOrClamp(by: 24.hours) } + + /// A private helper method for multiplying the TimeInterval value by a factor + /// and clamping the result to prevent overflow. If the multiplication results in + /// overflow, the greatest finite magnitude value of TimeInterval is returned. + /// + /// - Parameter factor: The multiplier to apply to the time interval. + /// - Returns: The multiplied time interval, clamped to the greatest finite magnitude if necessary. + private func multiplyOrClamp(by factor: TimeInterval) -> TimeInterval { + guard factor != 0 else { + return 0 + } + let multiplied = TimeInterval(self) * factor + if multiplied / factor != TimeInterval(self) { + return TimeInterval.greatestFiniteMagnitude + } + return multiplied + } +} + +/// An extension for FixedWidthInteger that provides a more semantic and expressive +/// API for converting time representations into TimeInterval's default unit: seconds. +public extension FixedWidthInteger { + + /// A helper property that returns the current value as a direct representation in seconds. + var seconds: TimeInterval { return TimeInterval(self) } + + /// A property that converts the given numeric value of minutes into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var minutes: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: 60) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } + + /// A property that converts the given numeric value of hours into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var hours: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: Self(60.minutes)) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } + + /// A property that converts the given numeric value of days into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var days: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: Self(24.hours)) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } +} + diff --git a/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift new file mode 100644 index 0000000000..4877d5eae8 --- /dev/null +++ b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.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. + */ + +import Foundation + +/// `PerformancePresetOverride` is a public structure that allows you to customize +/// performance presets by setting optional limits. If the limits are not provided, the default values from +/// the `PerformancePreset` object will be used. +public struct PerformancePresetOverride { + + /// An optional value representing the maximum allowed file size in bytes. + /// If not provided, the default value from the `PerformancePreset` object is used. + let maxFileSize: UInt64? + + /// An optional value representing the maximum allowed object size in bytes. + /// If not provided, the default value from the `PerformancePreset` object is used. + let maxObjectSize: UInt64? + + /// Initializes a new `PerformancePresetOverride` instance with the provided + /// maximum file size and maximum object size limits. + /// + /// - Parameters: + /// - maxFileSize: The maximum allowed file size in bytes, or `nil` to use the default value from `PerformancePreset`. + /// - maxObjectSize: The maximum allowed object size in bytes, or `nil` to use the default value from `PerformancePreset`. + public init(maxFileSize: UInt64?, maxObjectSize: UInt64?) { + self.maxFileSize = maxFileSize + self.maxObjectSize = maxObjectSize + } +} diff --git a/Sources/Datadog/Utils/SwiftExtensions.swift b/Sources/Datadog/Utils/SwiftExtensions.swift index d28af606a1..d1fbe0bb3f 100644 --- a/Sources/Datadog/Utils/SwiftExtensions.swift +++ b/Sources/Datadog/Utils/SwiftExtensions.swift @@ -101,41 +101,3 @@ extension Array { 0 <= index && index < count ? self[index] : nil } } - -// MARK: - Time Convinience - -public extension TimeInterval { - var seconds: TimeInterval { return TimeInterval(self) } - var minutes: TimeInterval { return self.multiplyOrClamp(by: 60) } - var hours: TimeInterval { return self.multiplyOrClamp(by: 60.minutes) } - var days: TimeInterval { return self.multiplyOrClamp(by: 24.hours) } - - private func multiplyOrClamp(by factor: TimeInterval) -> TimeInterval { - guard factor != 0 else { - return 0 - } - - let multiplied = TimeInterval(self) * factor - if multiplied / factor != TimeInterval(self) { - return TimeInterval.greatestFiniteMagnitude - } - return multiplied - } -} - -public extension FixedWidthInteger { - var seconds: TimeInterval { return TimeInterval(self) } - var minutes: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: 60).partialValue) } - var hours: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: Self(60.minutes)).partialValue) } - var days: TimeInterval { return TimeInterval(self.multipliedReportingOverflow(by: Self(24.hours)).partialValue) } -} - -// MARK: - Bytes Size Convenience - -public extension FixedWidthInteger { - private var base: Self { 1_024 } - var KB: Self { return self.multipliedReportingOverflow(by: base).partialValue } - var MB: Self { return self.KB.multipliedReportingOverflow(by: base).partialValue } - var GB: Self { return self.MB.multipliedReportingOverflow(by: base).partialValue } - var bytes: Self { return self } -} diff --git a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift index 1a2988da57..f7d1ccc955 100644 --- a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -188,4 +188,50 @@ class DatadogCoreTests: XCTestCase { ) XCTAssertEqual(requestBuilderSpy.requestParameters.count, 3, "It should send 3 requests") } + + func testWhenPerformancePresetOverrideIsNotProvided() throws { + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), + initialConsent: .granted, + userInfoProvider: .mockAny(), + performance: .mockRandom(), + httpClient: .mockAny(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny() + ) + try core.register( + feature: FeatureMock( + name: "mock", + performanceOverride: nil + ) + ) + let feature = core.v2Features.values.first + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, UInt64(512).KB) + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, UInt64(4).MB) + } + + func testWhenPerformancePresetOverrideIsProvided() throws { + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), + initialConsent: .granted, + userInfoProvider: .mockAny(), + performance: .mockRandom(), + httpClient: .mockAny(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny() + ) + try core.register( + feature: FeatureMock( + name: "mock", + performanceOverride: PerformancePresetOverride(maxFileSize: 123, maxObjectSize: 456) + ) + ) + let feature = core.v2Features.values.first + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, 456) + XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, 123) + } } diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift b/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift new file mode 100644 index 0000000000..7ff3b71d07 --- /dev/null +++ b/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import XCTest +@testable import Datadog + +final class FixedWidthIntegerConvinienceTests: XCTestCase { + func test_Bytes() { + let value: Int = 1_000 + XCTAssertEqual(value.bytes, 1_000) + } + + func test_Kilobytes() { + let value: Int = 1 + XCTAssertEqual(value.KB, 1_024) + } + + func test_Megabytes() { + let value: Int = 1 + XCTAssertEqual(value.MB, 1_048_576) + } + + func test_Gigabytes() { + let value: Int = 1 + XCTAssertEqual(value.GB, 1_073_741_824) + } + + func test_OverflowKilobytes() { + let value = UInt64.max / 1_024 + XCTAssertEqual(value.KB, UInt64.max &- 1_023) + } + + func test_OverflowMegabytes() { + let value = UInt64.max / (1_024 * 1_024) + XCTAssertEqual(value.MB, UInt64.max &- 1_048_575) + } + + func test_OverflowGigabytes() { + let value = UInt64.max / (1_024 * 1_024 * 1_024) + XCTAssertEqual(value.GB, UInt64.max &- 1_073_741_823) + } +} diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift b/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift new file mode 100644 index 0000000000..ad7d1cbd3e --- /dev/null +++ b/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift @@ -0,0 +1,42 @@ +/* + * 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. + */ + +import XCTest +@testable import Datadog + +final class TimeIntervalConvinienceTests: XCTestCase { + func test_Seconds() { + XCTAssertEqual(TimeInterval(30).seconds, 30) + XCTAssertEqual(Int(30).seconds, 30) + } + + func test_Minutes() { + XCTAssertEqual(TimeInterval(2).minutes, 120) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Hours() { + XCTAssertEqual(TimeInterval(3).hours, 10_800) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Days() { + XCTAssertEqual(TimeInterval(1).days, 86_400) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Overflow() { + let timeInterval = TimeInterval.greatestFiniteMagnitude + XCTAssertEqual(timeInterval.minutes, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(timeInterval.hours, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(timeInterval.days, TimeInterval.greatestFiniteMagnitude) + + let integerTimeInterval = Int.max + XCTAssertEqual(integerTimeInterval.minutes, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(integerTimeInterval.hours, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(integerTimeInterval.days, TimeInterval.greatestFiniteMagnitude) + } +} From 5dc8f75ed826b4f04d3d28f4c4b84f65227a5643 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 27 Mar 2023 13:48:02 +0100 Subject: [PATCH 51/72] REPLAY-1459 Fix linting --- .../DatadogInternal/Extensions/TimeInterval+Convinience.swift | 2 -- Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift b/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift index 14c3ba95f4..9dfdb783bb 100644 --- a/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift +++ b/Sources/Datadog/DatadogInternal/Extensions/TimeInterval+Convinience.swift @@ -45,7 +45,6 @@ public extension TimeInterval { /// An extension for FixedWidthInteger that provides a more semantic and expressive /// API for converting time representations into TimeInterval's default unit: seconds. public extension FixedWidthInteger { - /// A helper property that returns the current value as a direct representation in seconds. var seconds: TimeInterval { return TimeInterval(self) } @@ -70,4 +69,3 @@ public extension FixedWidthInteger { return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) } } - diff --git a/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift index 4877d5eae8..426f974fc3 100644 --- a/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift +++ b/Sources/Datadog/DatadogInternal/PerformancePresetOverride.swift @@ -10,7 +10,6 @@ import Foundation /// performance presets by setting optional limits. If the limits are not provided, the default values from /// the `PerformancePreset` object will be used. public struct PerformancePresetOverride { - /// An optional value representing the maximum allowed file size in bytes. /// If not provided, the default value from the `PerformancePreset` object is used. let maxFileSize: UInt64? From d557b4497bd5ead1f3b845d0fefbde85f0adb152 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 27 Mar 2023 14:23:44 +0100 Subject: [PATCH 52/72] REPLAY-1459 Update test --- Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index f6869d59f5..a812e1d1ef 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -143,8 +143,5 @@ class PerformancePresetTests: XCTestCase { let updatedPreset = preset.updated(with: PerformancePresetOverride(maxFileSize: 0, maxObjectSize: 0)) XCTAssertNotEqual(preset.maxFileSize, updatedPreset.maxFileSize) XCTAssertNotEqual(preset.maxObjectSize, updatedPreset.maxObjectSize) - - let notUpdatedPreset = preset.updated(with: nil) - XCTAssertEqual(preset, notUpdatedPreset) } } From 0a9a67c057e6169a34ca4da0777d9c9b350a64be Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 27 Mar 2023 14:26:36 +0100 Subject: [PATCH 53/72] REPLAY-1459 Change signature to non-optional --- Sources/Datadog/Core/PerformancePreset.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index 6648281746..e13322235d 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -126,15 +126,15 @@ internal extension PerformancePreset { self.uploadDelayChangeRate = uploadDelayFactors.changeRate } - func updated(with: PerformancePresetOverride?) -> PerformancePreset { + func updated(with: PerformancePresetOverride) -> PerformancePreset { return PerformancePreset( - maxFileSize: with?.maxFileSize ?? maxFileSize, + maxFileSize: with.maxFileSize ?? maxFileSize, maxDirectorySize: maxDirectorySize, maxFileAgeForWrite: maxFileAgeForWrite, minFileAgeForRead: minFileAgeForRead, maxFileAgeForRead: maxFileAgeForRead, maxObjectsInFile: maxObjectsInFile, - maxObjectSize: with?.maxObjectSize ?? maxObjectSize, + maxObjectSize: with.maxObjectSize ?? maxObjectSize, initialUploadDelay: initialUploadDelay, minUploadDelay: minUploadDelay, maxUploadDelay: maxUploadDelay, From 05a8278b1d9a397b260083a88e52311e0166bcbc Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Mon, 27 Mar 2023 10:34:25 -0400 Subject: [PATCH 54/72] RUMM-2872 Apply code review feedback --- .../KioskSendEventsViewController.swift | 4 +- .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 4 +- .../Scopes/RUMApplicationScope.swift | 9 ++-- .../RUMMonitor/Scopes/RUMSessionScope.swift | 47 ++++++++++--------- Sources/Datadog/RUMMonitor.swift | 5 +- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift index aeb6a789ea..7f39cb5a9e 100644 --- a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift +++ b/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift @@ -26,10 +26,10 @@ internal class KioskSendEventsViewController: UIViewController { rumMonitor.stopView(viewController: self) } - @IBAction func didTapDownloadResourceButton(_ sender: Any) { + @IBAction func didTapDownloadResourceButton(_ sender: UIButton) { rumMonitor.addUserAction( type: .tap, - name: (sender as! UIButton).currentTitle!, + name: sender.currentTitle!, attributes: ["button.description": String(describing: sender)] ) diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index ee7d35b9b6..84eba09128 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -29,8 +29,8 @@ internal struct RUMApplicationStartCommand: RUMCommand { internal struct RUMStopSessionCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] = [:] - var canStartBackgroundView = false // no, stopping a session should not start a backgorund session - var isUserInteraction = false + let canStartBackgroundView = false // no, stopping a session should not start a backgorund session + let isUserInteraction = false init(time: Date) { self.time = time diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 7091648a3a..50b2ad2f21 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -66,15 +66,18 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // that need a refresh sessionScopes = sessionScopes.compactMap({ scope in if scope.process(command: command, context: context, writer: writer) { - // Returned true, keep the scope around, it still has work to do. + // proccss(command:context:writer) returned true, so keep the scope around + // as it it still has work to do. return scope } + // proccss(command:context:writer) returned false, but if the scope is still active + // it means we timed out or expired and we need to refresh the session if scope.isActive { - // False, but still active means we timed out or expired, refresh the session return refresh(expiredSession: scope, on: command, context: context, writer: writer) } - // Else, inactive and done processing events, remove + + // Else, an inactive scope is done processing events and can be removed return nil }) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 9c22e616c4..b7a4fcb4bd 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -177,28 +177,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Start view scope explicitly on receiving "start view" command startView(on: startViewCommand, context: context) } else if !hasActiveView { - // Otherwise, if there is no active view scope, consider starting artificial scope for handling this command - let handlingRule = RUMOffViewEventsHandlingRule( - sessionState: state, - isAppInForeground: context.applicationStateHistory.currentSnapshot.state.isRunningInForeground, - isBETEnabled: backgroundEventTrackingEnabled - ) - - switch handlingRule { - case .handleInBackgroundView where command.canStartBackgroundView: - startBackgroundView(on: command, context: context) - default: - if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) - // As no view scope will handle this command, warn the user on dropping it. - DD.logger.warn( - """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. - """ - ) - } - } + handleOffViewCommand(command: command, context: context) } } @@ -267,6 +246,30 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { ) } + private func handleOffViewCommand(command: RUMCommand, context: DatadogContext) { + let handlingRule = RUMOffViewEventsHandlingRule( + sessionState: state, + isAppInForeground: context.applicationStateHistory.currentSnapshot.state.isRunningInForeground, + isBETEnabled: backgroundEventTrackingEnabled + ) + + switch handlingRule { + case .handleInBackgroundView where command.canStartBackgroundView: + startBackgroundView(on: command, context: context) + default: + if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) + // As no view scope will handle this command, warn the user on dropping it. + DD.logger.warn( + """ + \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the + DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using + the RumMonitor.startView() and RumMonitor.stopView() methods. + """ + ) + } + } + } + private func startBackgroundView(on command: RUMCommand, context: DatadogContext) { let isStartingInitialView = isInitialSession && !state.hasTrackedAnyView viewScopes.append( diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index cdc19d7f21..210d7c157d 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -636,10 +636,11 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { core.set(feature: "rum", attributes: { self.queue.sync { let context = self.applicationScope.activeSession?.viewScopes.last?.context ?? - self.applicationScope.activeSession?.context ?? - self.applicationScope.context + self.applicationScope.activeSession?.context ?? + self.applicationScope.context guard context.sessionID != .nullUUID else { + // if Session was sampled or not yet started return [:] } From c8a62882d904a6d03d71b4f59df445ab2d785e34 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 11:42:56 +0100 Subject: [PATCH 55/72] REPLAY-1459 PR fixes --- .../UIStepperRecorderTests.swift | 2 +- .../Datadog/Core/PerformancePresetTests.swift | 2 +- .../DatadogCore/DatadogCoreTests.swift | 25 +++++-------------- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift index 009d5ede4a..054cb11645 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorderTests.swift @@ -34,7 +34,7 @@ class UIStepperRecorderTests: XCTestCase { XCTAssertTrue(semantics is SpecificElement) XCTAssertEqual(semantics.subtreeStrategy, .ignore, "Stepper's subtree should not be recorded") - let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIStepperWireframesBuilder) + _ = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIStepperWireframesBuilder) } func testWhenViewIsNotOfExpectedType() { diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index a812e1d1ef..f761f9a789 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -140,7 +140,7 @@ class PerformancePresetTests: XCTestCase { func testPresetsUpdate() { let preset = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .mockRandom(), bundleType: .mockRandom()) - let updatedPreset = preset.updated(with: PerformancePresetOverride(maxFileSize: 0, maxObjectSize: 0)) + let updatedPreset = preset.updated(with: PerformancePresetOverride(maxFileSize: .mockRandom(), maxObjectSize: .mockRandom())) XCTAssertNotEqual(preset.maxFileSize, updatedPreset.maxFileSize) XCTAssertNotEqual(preset.maxObjectSize, updatedPreset.maxObjectSize) } diff --git a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift index f7d1ccc955..e12f11f042 100644 --- a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -189,7 +189,7 @@ class DatadogCoreTests: XCTestCase { XCTAssertEqual(requestBuilderSpy.requestParameters.count, 3, "It should send 3 requests") } - func testWhenPerformancePresetOverrideIsNotProvided() throws { + func testWhenPerformancePresetOverrideIsProvided_itOverridesPresets() throws { let core = DatadogCore( directory: temporaryCoreDirectory, dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), @@ -201,36 +201,23 @@ class DatadogCoreTests: XCTestCase { contextProvider: .mockAny(), applicationVersion: .mockAny() ) + let name = "mock" try core.register( feature: FeatureMock( - name: "mock", + name: name, performanceOverride: nil ) ) - let feature = core.v2Features.values.first + var feature = core.v2Features.values.first XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, UInt64(512).KB) XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, UInt64(4).MB) - } - - func testWhenPerformancePresetOverrideIsProvided() throws { - let core = DatadogCore( - directory: temporaryCoreDirectory, - dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), - initialConsent: .granted, - userInfoProvider: .mockAny(), - performance: .mockRandom(), - httpClient: .mockAny(), - encryption: nil, - contextProvider: .mockAny(), - applicationVersion: .mockAny() - ) try core.register( feature: FeatureMock( - name: "mock", + name: name, performanceOverride: PerformancePresetOverride(maxFileSize: 123, maxObjectSize: 456) ) ) - let feature = core.v2Features.values.first + feature = core.v2Features.values.first XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, 456) XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, 123) } From 5e6329cbff0ee967fb6e182fed19ed3226f6605e Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 13 Mar 2023 14:17:41 +0000 Subject: [PATCH 56/72] REPLAY-1345 Improve tint behaviour --- .../NodeRecorders/UIImageViewRecorder.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 11d5bf7548..a5c3b8cebd 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -19,8 +19,12 @@ internal struct UIImageViewRecorder: NodeRecorder { } internal init( - tintColorProvider: @escaping (UIImageView) -> UIColor? = { _ in - return nil + tintColorProvider: @escaping (UIImageView) -> UIColor? = { imageView in + if #available(iOS 13.0, *) { + return imageView.image?.isSymbolImage == true ? imageView.tintColor : nil + } else { + return nil + } }, shouldRecordImagePredicate: @escaping (UIImageView) -> Bool = { imageView in if #available(iOS 13.0, *) { From f7cb74b6f9cd716da78f9fcab812563f12908ab1 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 21 Mar 2023 10:05:20 +0000 Subject: [PATCH 57/72] REPLAY-1345 Add scaling --- .../Utilities/ImageDataProvider.swift | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 655c2cc167..dc58c6026a 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -39,17 +39,17 @@ internal final class ImageDataProvider: ImageDataProviding { guard var image = image else { return "" } - if #available(iOS 13.0, *), let tintColor = tintColor { - image = image.withTintColor(tintColor) - } - var identifier = image.srIdentifier if let tintColorIdentifier = tintColor?.srIdentifier { identifier += tintColorIdentifier } if let base64EncodedImage = cache[identifier] { return base64EncodedImage - } else if let base64EncodedImage = image.pngData()?.base64EncodedString(), base64EncodedImage.count <= maxBytesSize { + } else { + if #available(iOS 13.0, *), let tintColor = tintColor { + image = image.withTintColor(tintColor) + } + let base64EncodedImage = image.scaledToMaxSize(maxBytesSize).base64EncodedString() cache[identifier, base64EncodedImage.count] = base64EncodedImage return base64EncodedImage } else { @@ -74,6 +74,26 @@ extension UIImage { var srIdentifier: String { return "\(hash)" } + + func scaledToMaxSize(_ maxSizeInBytes: Int) -> Data { + guard let imageData = pngData() else { + return Data() + } + guard imageData.count >= maxSizeInBytes else { + return imageData + } + let percentage: CGFloat = sqrt(CGFloat(maxSizeInBytes) / CGFloat(imageData.count)) + return scaledImage(by: percentage).pngData() ?? Data() + } + + func scaledImage(by percentage: CGFloat) -> UIImage { + let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) + let format = UIGraphicsImageRendererFormat() + let renderer = UIGraphicsImageRenderer(size: newSize, format: format) + return renderer.image { context in + draw(in: CGRect(origin: .zero, size: newSize)) + } + } } extension UIColor { From d5a8510785b0fb7e4ecf565a7118dfea5262bb36 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 09:57:34 +0100 Subject: [PATCH 58/72] REPLAY-1345 WIP --- .../Utilities/ImageDataProvider.swift | 27 +--------- .../Sources/Utilities/UIImage+Scaling.swift | 51 +++++++++++++++++++ .../Utilties/ImageDataProviderTests.swift | 10 ---- .../Utilities/UIImage+ScalingTests.swift | 35 +++++++++++++ 4 files changed, 88 insertions(+), 35 deletions(-) create mode 100644 DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift create mode 100644 DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index dc58c6026a..48d80949e5 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -25,7 +25,7 @@ internal final class ImageDataProvider: ImageDataProviding { internal init( cache: Cache = .init(), - maxBytesSize: Int = 10_000 + maxBytesSize: Int = 15_360 // 10.0 KB ) { self.cache = cache self.maxBytesSize = maxBytesSize @@ -49,12 +49,9 @@ internal final class ImageDataProvider: ImageDataProviding { if #available(iOS 13.0, *), let tintColor = tintColor { image = image.withTintColor(tintColor) } - let base64EncodedImage = image.scaledToMaxSize(maxBytesSize).base64EncodedString() + let base64EncodedImage = image.scaledDownToApproximateSize(maxBytesSize).base64EncodedString() cache[identifier, base64EncodedImage.count] = base64EncodedImage return base64EncodedImage - } else { - cache[identifier] = "" - return "" } } } @@ -74,26 +71,6 @@ extension UIImage { var srIdentifier: String { return "\(hash)" } - - func scaledToMaxSize(_ maxSizeInBytes: Int) -> Data { - guard let imageData = pngData() else { - return Data() - } - guard imageData.count >= maxSizeInBytes else { - return imageData - } - let percentage: CGFloat = sqrt(CGFloat(maxSizeInBytes) / CGFloat(imageData.count)) - return scaledImage(by: percentage).pngData() ?? Data() - } - - func scaledImage(by percentage: CGFloat) -> UIImage { - let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) - let format = UIGraphicsImageRendererFormat() - let renderer = UIGraphicsImageRenderer(size: newSize, format: format) - return renderer.image { context in - draw(in: CGRect(origin: .zero, size: newSize)) - } - } } extension UIColor { diff --git a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift new file mode 100644 index 0000000000..9dd0b616f0 --- /dev/null +++ b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift @@ -0,0 +1,51 @@ +/* + * 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. + */ + +import UIKit + +extension UIImage { + /** + Returns a scaled version of the image that is approximately equal to the size specified by the `maxSizeInBytes` parameter. + The approximate size is calculated based on the bitmap dimensions of the image, + but does not take into account the size of the PNG header or any compression that may be applied. + + - Parameters: + - maxSizeInBytes: The maximum size, in bytes, of the scaled image. + + - Returns: The data object containing the scaled image, or an empty data object if the image data cannot be converted to PNG data or if the scaled image cannot be converted to PNG data. + */ + func scaledDownToApproximateSize(_ maxSizeInBytes: Int, _ maxIterations: Int = 20) -> Data { + guard let imageData = pngData() else { + return Data() + } + guard imageData.count >= maxSizeInBytes else { + return imageData + } + let percentage: CGFloat = CGFloat(maxSizeInBytes) / CGFloat(imageData.count) + var scaledData = scaledImage(by: percentage)?.pngData() ?? Data() + + var iterations = 0, scale: Double = 1 + while scaledData.count > maxSizeInBytes && iterations < maxIterations { + scale *= 0.9 + let newScaledData = scaledImage(by: scale)?.pngData() ?? Data() + if newScaledData.count <= scaledData.count { + scaledData = newScaledData + } + iterations += 1 + } + return scaledData.count < imageData.count ? scaledData : imageData + } + + private func scaledImage(by percentage: CGFloat) -> UIImage? { + let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) + UIGraphicsBeginImageContextWithOptions(newSize, false, 1) + draw(in: CGRect(origin: .zero, size: newSize)) + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return scaledImage + } +} + diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift index a36ab3e549..d7f5d241bc 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/ImageDataProviderTests.swift @@ -41,16 +41,6 @@ class ImageDataProviderTests: XCTestCase { XCTAssertGreaterThan(imageData.count, 0) } - func test_ignoresAboveSize() throws { - let sut = ImageDataProvider( - maxBytesSize: 1 - ) - let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil) - - let imageData = try XCTUnwrap(sut.contentBase64String(of: image)) - XCTAssertEqual(imageData.count, 0) - } - func test_imageIdentifierConsistency() { var ids = Set() for _ in 0..<100 { diff --git a/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift new file mode 100644 index 0000000000..fe11bb4f54 --- /dev/null +++ b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift @@ -0,0 +1,35 @@ +/* + * 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. + */ + +import Foundation +import XCTest +@testable import DatadogSessionReplay + +@available(iOS 13.0, *) +class UIImageScalingTests: XCTestCase { + + var sut: (image: UIImage, pngData: Data) { + guard let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil), let imageData = image.pngData() else { + XCTFail("Failed to load image") + return (UIImage(), Data()) + } + return (image, imageData) + } + + func testScaledToApproximateSize_ReturnsOriginalImageData_IfSizeIsSmallerOrEqualToAnticipatedMaxSize() { + let dataSize = sut.pngData.count + let maxSize = dataSize + 100 + let scaledData = sut.image.scaledDownToApproximateSize(maxSize) + XCTAssertEqual(scaledData, sut.pngData) + } + + func testScaledToApproximateSize_ScalesImageToSmallerSize_IfSizeIsLargerThanAnticipatedMaxSize() { + let dataSize = sut.pngData.count + let maxSize = dataSize - 100 + let scaledData = sut.image.scaledDownToApproximateSize(maxSize) + XCTAssertTrue(scaledData.count < dataSize) + } +} From dc04eb2cb15bb187e54487814179d877e10674a9 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 12:28:12 +0100 Subject: [PATCH 59/72] REPLAY-1345 Improve algorithm --- .../Sources/Utilities/UIImage+Scaling.swift | 40 +++++++++++-------- .../Utilities/UIImage+ScalingTests.swift | 1 - 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift index 9dd0b616f0..09f032d67b 100644 --- a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift +++ b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift @@ -17,35 +17,43 @@ extension UIImage { - Returns: The data object containing the scaled image, or an empty data object if the image data cannot be converted to PNG data or if the scaled image cannot be converted to PNG data. */ - func scaledDownToApproximateSize(_ maxSizeInBytes: Int, _ maxIterations: Int = 20) -> Data { + func scaledDownToApproximateSize(_ desiredSizeInBytes: Int) -> Data { guard let imageData = pngData() else { return Data() } - guard imageData.count >= maxSizeInBytes else { + guard imageData.count > desiredSizeInBytes else { return imageData } - let percentage: CGFloat = CGFloat(maxSizeInBytes) / CGFloat(imageData.count) - var scaledData = scaledImage(by: percentage)?.pngData() ?? Data() + var scaledImage = scaledImage(by: CGFloat(desiredSizeInBytes) / CGFloat(imageData.count)) - var iterations = 0, scale: Double = 1 - while scaledData.count > maxSizeInBytes && iterations < maxIterations { - scale *= 0.9 - let newScaledData = scaledImage(by: scale)?.pngData() ?? Data() - if newScaledData.count <= scaledData.count { - scaledData = newScaledData + var scale: Double = 1 + let maxIterations = 20 + for _ in 0...maxIterations { + guard let scaledImageData = scaledImage.pngData() else { + return imageData + } + if scaledImageData.count <= desiredSizeInBytes { + return scaledImageData } - iterations += 1 + scale *= 0.9 + scaledImage = scaledImage.scaledImage(by: scale) + } + guard let scaledImageData = scaledImage.pngData() else { + return imageData } - return scaledData.count < imageData.count ? scaledData : imageData + return scaledImageData.count < imageData.count ? scaledImageData : imageData } - private func scaledImage(by percentage: CGFloat) -> UIImage? { + private func scaledImage(by percentage: CGFloat) -> UIImage { + guard percentage > 0 else { + return UIImage() + } let newSize = CGSize(width: size.width * percentage, height: size.height * percentage) - UIGraphicsBeginImageContextWithOptions(newSize, false, 1) + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) draw(in: CGRect(origin: .zero, size: newSize)) let scaledImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return scaledImage + + return scaledImage ?? UIImage() } } - diff --git a/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift index fe11bb4f54..2903e8f5c5 100644 --- a/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift +++ b/DatadogSessionReplay/Tests/Utilities/UIImage+ScalingTests.swift @@ -10,7 +10,6 @@ import XCTest @available(iOS 13.0, *) class UIImageScalingTests: XCTestCase { - var sut: (image: UIImage, pngData: Data) { guard let image = UIImage(named: "dd_logo_v_rgb", in: Bundle.module, compatibleWith: nil), let imageData = image.pngData() else { XCTFail("Failed to load image") From b41cac038900053991eb061e6e9cf1c944ff42ce Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 12:29:26 +0100 Subject: [PATCH 60/72] REPLAY-1345 Update max size value to use new semantics --- .../Sources/Recorder/Utilities/ImageDataProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 48d80949e5..796058aee0 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -25,7 +25,7 @@ internal final class ImageDataProvider: ImageDataProviding { internal init( cache: Cache = .init(), - maxBytesSize: Int = 15_360 // 10.0 KB + maxBytesSize: Int = 10.KB ) { self.cache = cache self.maxBytesSize = maxBytesSize From d3c67581e23203e27cceeee437511201625b7b32 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 12:41:06 +0100 Subject: [PATCH 61/72] REPLAY-1345 Improve docs --- .../Utilities/ImageDataProvider.swift | 8 ++--- .../Sources/Utilities/UIImage+Scaling.swift | 35 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 796058aee0..ce8eb14e5f 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -21,14 +21,14 @@ internal protocol ImageDataProviding { internal final class ImageDataProvider: ImageDataProviding { private var cache: Cache - private let maxBytesSize: Int + private let desiredMaxBytesSize: Int internal init( cache: Cache = .init(), - maxBytesSize: Int = 10.KB + desiredMaxBytesSize: Int = 10.KB ) { self.cache = cache - self.maxBytesSize = maxBytesSize + self.desiredMaxBytesSize = desiredMaxBytesSize } func contentBase64String( @@ -49,7 +49,7 @@ internal final class ImageDataProvider: ImageDataProviding { if #available(iOS 13.0, *), let tintColor = tintColor { image = image.withTintColor(tintColor) } - let base64EncodedImage = image.scaledDownToApproximateSize(maxBytesSize).base64EncodedString() + let base64EncodedImage = image.scaledDownToApproximateSize(desiredMaxBytesSize).base64EncodedString() cache[identifier, base64EncodedImage.count] = base64EncodedImage return base64EncodedImage } diff --git a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift index 09f032d67b..96634c6abf 100644 --- a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift +++ b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift @@ -7,16 +7,24 @@ import UIKit extension UIImage { - /** - Returns a scaled version of the image that is approximately equal to the size specified by the `maxSizeInBytes` parameter. - The approximate size is calculated based on the bitmap dimensions of the image, - but does not take into account the size of the PNG header or any compression that may be applied. - - - Parameters: - - maxSizeInBytes: The maximum size, in bytes, of the scaled image. - - - Returns: The data object containing the scaled image, or an empty data object if the image data cannot be converted to PNG data or if the scaled image cannot be converted to PNG data. - */ + /// Scales down the image to an approximate file size in bytes. + /// + /// - Parameter desiredSizeInBytes: The target file size in bytes. + /// - Returns: A Data object representing the scaled down image as PNG data. + /// + /// This function takes the desired file size in bytes as input and scales down the image iteratively + /// until the resulting PNG data size is less than or equal to the specified target size. + /// + /// Note: The function will return the original image data if it is already smaller than the desired size, + /// or if it fails to generate a smaller image. + /// + /// Example usage: + /// + /// let originalImage = UIImage(named: "exampleImage") + /// let desiredSizeInBytes = 10240 // 10 KB + /// if let imageData = originalImage?.scaledDownToApproximateSize(desiredSizeInBytes) { + /// // Use the scaled down image data. + /// } func scaledDownToApproximateSize(_ desiredSizeInBytes: Int) -> Data { guard let imageData = pngData() else { return Data() @@ -44,6 +52,13 @@ extension UIImage { return scaledImageData.count < imageData.count ? scaledImageData : imageData } + /// Scales the image by a given percentage. + /// + /// - Parameter percentage: The scaling factor to apply, where 1.0 represents the original size. + /// - Returns: A UIImage object representing the scaled image, or an empty UIImage if the percentage is less than or equal to zero. + /// + /// This private helper function takes a CGFloat percentage as input and scales the image accordingly. + /// It ensures that the resulting image has a size proportional to the original one, maintaining its aspect ratio. private func scaledImage(by percentage: CGFloat) -> UIImage { guard percentage > 0 else { return UIImage() From 8a34b3244400ceaa661dba094ee990eec3ea3ce9 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 28 Mar 2023 13:42:50 +0100 Subject: [PATCH 62/72] REPLAY-1345 Improvements --- .../Sources/Recorder/Utilities/ImageDataProvider.swift | 2 +- DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index ce8eb14e5f..448c582684 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -25,7 +25,7 @@ internal final class ImageDataProvider: ImageDataProviding { internal init( cache: Cache = .init(), - desiredMaxBytesSize: Int = 10.KB + desiredMaxBytesSize: Int = 15.KB ) { self.cache = cache self.desiredMaxBytesSize = desiredMaxBytesSize diff --git a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift index 96634c6abf..bb273df355 100644 --- a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift +++ b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift @@ -32,7 +32,7 @@ extension UIImage { guard imageData.count > desiredSizeInBytes else { return imageData } - var scaledImage = scaledImage(by: CGFloat(desiredSizeInBytes) / CGFloat(imageData.count)) + var scaledImage = scaledImage(by: sqrt(CGFloat(desiredSizeInBytes) / CGFloat(imageData.count))) var scale: Double = 1 let maxIterations = 20 From ff06c26d138835af4568c45054eb49a2da6076a3 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 29 Mar 2023 12:20:21 +0200 Subject: [PATCH 63/72] REPLAY-1421 Use better fallback value for font definition in text wireframes --- .../Processor/SRDataModelsBuilder/WireframesBuilder.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift index 98b7d272d2..8eef1189a1 100644 --- a/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/SRDataModelsBuilder/WireframesBuilder.swift @@ -25,7 +25,10 @@ internal class WireframesBuilder { /// The color (solid red) to use when the actual color conversion goes wrong. static let color = "#FF0000FF" /// The font family to use when the actual one cannot be read. - static let fontFamily = "-apple-system, Roboto, Helvetica, Arial" + /// + /// REPLAY-1421: This definition will promote SF font when running player in Safari, then “BlinkMacSystemFont” in macOS Chrome and + /// will ultimately fallback to “Roboto” or any “sans-serif” in other web browsers. + static let fontFamily = "-apple-system, BlinkMacSystemFont, 'Roboto', sans-serif" /// The font size to use when the actual one cannot be read. static let fontSize: CGFloat = 10 } From 8d7a75a4a744b9f447826e4df307a0bdd2d396fa Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 30 Mar 2023 10:01:46 +0100 Subject: [PATCH 64/72] REPLAY-1345 PR fixes --- .../Sources/Utilities/UIImage+Scaling.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift index bb273df355..c926bba520 100644 --- a/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift +++ b/DatadogSessionReplay/Sources/Utilities/UIImage+Scaling.swift @@ -20,11 +20,11 @@ extension UIImage { /// /// Example usage: /// - /// let originalImage = UIImage(named: "exampleImage") - /// let desiredSizeInBytes = 10240 // 10 KB - /// if let imageData = originalImage?.scaledDownToApproximateSize(desiredSizeInBytes) { - /// // Use the scaled down image data. - /// } + /// let originalImage = UIImage(named: "exampleImage") + /// let desiredSizeInBytes = 10240 // 10 KB + /// if let imageData = originalImage?.scaledDownToApproximateSize(desiredSizeInBytes) { + /// // Use the scaled down image data. + /// } func scaledDownToApproximateSize(_ desiredSizeInBytes: Int) -> Data { guard let imageData = pngData() else { return Data() @@ -32,7 +32,10 @@ extension UIImage { guard imageData.count > desiredSizeInBytes else { return imageData } - var scaledImage = scaledImage(by: sqrt(CGFloat(desiredSizeInBytes) / CGFloat(imageData.count))) + // Initial scale is approximatation based on the average side of square for given size ratio. + // When running experiments it appeared to be closer to desired scale than using just a size ratio. + let initialScale = sqrt(CGFloat(desiredSizeInBytes) / CGFloat(imageData.count)) + var scaledImage = scaledImage(by: initialScale) var scale: Double = 1 let maxIterations = 20 From fbe5bf365d55c8bba2be46d958ca663d324bde6c Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 29 Mar 2023 17:00:40 +0200 Subject: [PATCH 65/72] Upgrade to Framer 1.0.0 --- .../SRSnapshotTests.xcodeproj/project.pbxproj | 24 ++++---- .../Utils/ImageRendering.swift | 55 ++++++++++++------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index 37e4b14868..f1841ccbe6 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 616C37DA299F6913005E0472 /* InputElements.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 616C37D9299F6913005E0472 /* InputElements.storyboard */; }; 6196D32429AF7EB2002EACAF /* InputElements-DatePickers.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6196D32329AF7EB2002EACAF /* InputElements-DatePickers.storyboard */; }; - 619C49B229952108006B66A6 /* Framer in Frameworks */ = {isa = PBXBuildFile; productRef = 619C49B129952108006B66A6 /* Framer */; }; 619C49B429952E12006B66A6 /* SnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619C49B329952E12006B66A6 /* SnapshotTestCase.swift */; }; 619C49B72995512A006B66A6 /* ImageComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619C49B62995512A006B66A6 /* ImageComparison.swift */; }; 619C49B9299551F5006B66A6 /* _snapshots_ in Resources */ = {isa = PBXBuildFile; fileRef = 619C49B8299551F5006B66A6 /* _snapshots_ */; }; @@ -22,6 +21,7 @@ 61B3BC652993BEAF0032C78A /* SRSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B3BC642993BEAF0032C78A /* SRSnapshotTests.swift */; }; 61B3BC6D2993C06D0032C78A /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = 61B3BC6C2993C06D0032C78A /* DatadogSessionReplay */; }; 61B634EB299A5DB3002BEABE /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B634EA299A5DB3002BEABE /* Fixtures.swift */; }; + 61C06D6029D487E7007521D8 /* Framer in Frameworks */ = {isa = PBXBuildFile; productRef = 61C06D5F29D487E7007521D8 /* Framer */; }; 61E7DFB7299A57A9001D7A3A /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E7DFB6299A57A9001D7A3A /* MenuViewController.swift */; }; 61E7DFB9299A5A3E001D7A3A /* Basic.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61E7DFB8299A5A3E001D7A3A /* Basic.storyboard */; }; 61E7DFBB299A5C9D001D7A3A /* BasicViewControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E7DFBA299A5C9D001D7A3A /* BasicViewControllers.swift */; }; @@ -66,6 +66,7 @@ buildActionMask = 2147483647; files = ( 61B3BC6D2993C06D0032C78A /* DatadogSessionReplay in Frameworks */, + 61C06D6029D487E7007521D8 /* Framer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -73,7 +74,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 619C49B229952108006B66A6 /* Framer in Frameworks */, 61EC1A37299CFD7E00224FB6 /* TestUtilities in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -172,6 +172,7 @@ name = SRHost; packageProductDependencies = ( 61B3BC6C2993C06D0032C78A /* DatadogSessionReplay */, + 61C06D5F29D487E7007521D8 /* Framer */, ); productName = SRHost; productReference = 61B3BC472993BE2E0032C78A /* SRHost.app */; @@ -192,7 +193,6 @@ ); name = SRSnapshotTests; packageProductDependencies = ( - 619C49B129952108006B66A6 /* Framer */, 61EC1A36299CFD7E00224FB6 /* TestUtilities */, ); productName = SRSnapshotTests; @@ -228,7 +228,7 @@ ); mainGroup = 61B3BC3E2993BE2E0032C78A; packageReferences = ( - 619C49B029952108006B66A6 /* XCRemoteSwiftPackageReference "Framing" */, + 61C06D5E29D487E7007521D8 /* XCRemoteSwiftPackageReference "Framer" */, ); productRefGroup = 61B3BC482993BE2E0032C78A /* Products */; projectDirPath = ""; @@ -557,26 +557,26 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 619C49B029952108006B66A6 /* XCRemoteSwiftPackageReference "Framing" */ = { + 61C06D5E29D487E7007521D8 /* XCRemoteSwiftPackageReference "Framer" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ncreated/Framing"; + repositoryURL = "https://github.com/ncreated/Framer"; requirement = { - branch = "ship-framer"; + branch = "ncreated-patch-1"; kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 619C49B129952108006B66A6 /* Framer */ = { - isa = XCSwiftPackageProductDependency; - package = 619C49B029952108006B66A6 /* XCRemoteSwiftPackageReference "Framing" */; - productName = Framer; - }; 61B3BC6C2993C06D0032C78A /* DatadogSessionReplay */ = { isa = XCSwiftPackageProductDependency; productName = DatadogSessionReplay; }; + 61C06D5F29D487E7007521D8 /* Framer */ = { + isa = XCSwiftPackageProductDependency; + package = 61C06D5E29D487E7007521D8 /* XCRemoteSwiftPackageReference "Framer" */; + productName = Framer; + }; 61EC1A36299CFD7E00224FB6 /* TestUtilities */ = { isa = XCSwiftPackageProductDependency; productName = TestUtilities; diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift index 772f700694..74469d8534 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift @@ -22,7 +22,12 @@ internal func renderImage(for wireframes: [SRWireframe]) -> UIImage { let frame = wireframes[0].toFrame() let canvas = FramerCanvas.create(size: CGSize(width: frame.width, height: frame.height)) - canvas.draw(blueprint: Blueprint(id: "snapshot", frames: wireframes.map { $0.toFrame() })) + canvas.draw( + blueprint: Blueprint( + id: "snapshot", + contents: wireframes.map { .frame($0.toFrame()) } + ) + ) return canvas.image } @@ -83,8 +88,8 @@ private extension SRImageWireframe { } } -private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> BlueprintFrameStyle { - var fs = BlueprintFrameStyle( +private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> BlueprintFrame.Style { + var fs = BlueprintFrame.Style( lineWidth: 0, lineColor: .clear, fillColor: .clear, @@ -106,34 +111,42 @@ private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> Bluepri return fs } -private func frameContent(text: String, textStyle: SRTextStyle?, textPosition: SRTextPosition?) -> BlueprintFrameContent { - var fc = BlueprintFrameContent( - text: text, - textColor: .clear, - font: .systemFont(ofSize: 8) - ) - - if let textStyle = textStyle { - fc.textColor = UIColor(hexString: textStyle.color) - fc.font = .systemFont(ofSize: CGFloat(textStyle.size)) - } +private func frameContent(text: String, textStyle: SRTextStyle?, textPosition: SRTextPosition?) -> BlueprintFrame.Content { + var horizontalAlignment: BlueprintFrame.Content.Alignment = .leading + var verticalAlignment: BlueprintFrame.Content.Alignment = .leading if let textPosition = textPosition { switch textPosition.alignment?.horizontal { - case .left?: fc.horizontalAlignment = .leading - case .center?: fc.horizontalAlignment = .center - case .right?: fc.horizontalAlignment = .trailing + case .left?: horizontalAlignment = .leading + case .center?: horizontalAlignment = .center + case .right?: horizontalAlignment = .trailing default: break } switch textPosition.alignment?.vertical { - case .top?: fc.verticalAlignment = .leading - case .center?: fc.verticalAlignment = .center - case .bottom?: fc.verticalAlignment = .trailing + case .top?: verticalAlignment = .leading + case .center?: verticalAlignment = .center + case .bottom?: verticalAlignment = .trailing default: break } } - return fc + return .init( + contentType: frameContentType(text: text, textStyle: textStyle), + horizontalAlignment: horizontalAlignment, + verticalAlignment: verticalAlignment + ) +} + +private func frameContentType(text: String, textStyle: SRTextStyle?) -> BlueprintFrame.Content.ContentType { + if let textStyle = textStyle { + return .text( + text: text, + color: UIColor(hexString: textStyle.color), + font: .systemFont(ofSize: CGFloat(textStyle.size)) + ) + } else { + return .text(text: text, color: .clear, font: .systemFont(ofSize: 8)) + } } private extension UIColor { From b7e33e6759df6902afce99c334c5f3daaab65e8b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 30 Mar 2023 10:20:47 +0100 Subject: [PATCH 66/72] REPLAY-1465 Enable image view snapshotting --- .../Utils/ImageRendering.swift | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift index 74469d8534..0430db8230 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift @@ -79,11 +79,7 @@ private extension SRImageWireframe { y: CGFloat(y), width: CGFloat(width), height: CGFloat(height), - style: .init(lineWidth: 1, lineColor: .black, fillColor: .red), - annotation: .init( - text: "IMG \(width) x \(height)", - style: .init(size: .small, position: .top, alignment: .trailing) - ) + content: frameContent(base64ImageString: base64) ) } } @@ -130,23 +126,30 @@ private func frameContent(text: String, textStyle: SRTextStyle?, textPosition: S } } - return .init( - contentType: frameContentType(text: text, textStyle: textStyle), - horizontalAlignment: horizontalAlignment, - verticalAlignment: verticalAlignment - ) -} - -private func frameContentType(text: String, textStyle: SRTextStyle?) -> BlueprintFrame.Content.ContentType { + let contentType: BlueprintFrame.Content.ContentType if let textStyle = textStyle { - return .text( + contentType = .text( text: text, color: UIColor(hexString: textStyle.color), font: .systemFont(ofSize: CGFloat(textStyle.size)) ) } else { - return .text(text: text, color: .clear, font: .systemFont(ofSize: 8)) + contentType = .text(text: text, color: .clear, font: .systemFont(ofSize: 8)) } + + return .init( + contentType: contentType, + horizontalAlignment: horizontalAlignment, + verticalAlignment: verticalAlignment + ) +} + +private func frameContent(base64ImageString: String?) -> BlueprintFrame.Content { + let base64Data = base64ImageString?.data(using: .utf8) + let imageData = Data(base64Encoded: base64Data!)! + + let contentType: BlueprintFrame.Content.ContentType = .image(image: UIImage(data: imageData, scale: UIScreen.main.scale)!) + return .init(contentType: contentType) } private extension UIColor { From 3598a49befbcc0ffd6feac0cc56d0eb9db955a4a Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 30 Mar 2023 12:24:35 +0100 Subject: [PATCH 67/72] REPLAY-1465 Add snapshot tests for images --- .../SRHost/Assets.xcassets/Contents.json | 6 + .../dd_logo.imageset/Contents.json | 21 ++ .../dd_logo.imageset/dd_logo.jpg | Bin 0 -> 7073 bytes .../SRHost/Fixtures/Fixtures.swift | 6 + .../SRHost/Fixtures/Images.storyboard | 281 ++++++++++++++++++ .../SRSnapshotTests.xcodeproj/project.pbxproj | 8 + .../xcschemes/SRSnapshotTests.xcscheme | 13 +- .../SRSnapshotTests/SRSnapshotTests.swift | 20 +- .../Utils/ImageRendering.swift | 9 +- 9 files changed, 358 insertions(+), 6 deletions(-) create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/Contents.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/Contents.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/dd_logo.jpg create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/Contents.json b/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/Contents.json b/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/Contents.json new file mode 100644 index 0000000000..b43d7f752a --- /dev/null +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "dd_logo.jpg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/dd_logo.jpg b/DatadogSessionReplay/SRSnapshotTests/SRHost/Assets.xcassets/dd_logo.imageset/dd_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..403084aeae30b8effe8f397afc647b6824852525 GIT binary patch literal 7073 zcmbt%1yodB_xFVXMnFJPq#J3aq=xR!p+UO4qy#|}iJ?P6T3~=tIs}w13F$_8>d!Nu#H4Km+^-nOS(aN~o(V|A*+m;Mw?}=m0Rn z@h8@QDgRH8*p^lv7RUr2APog9T-`m9SR9E}eLP+N;8-LkvamI?MB;oTW_L#>h{QvG z7p)Jb&ULOMa*b0O-s2_s6UN08R&h z+xPeP*SYuiw|M{nngoC!iU0V!BqH0nhm^L3W?ldQwa6Zm&bykq zoBg#82sxr!TLZvRApqd$0{|hi*I0)Czxu!Xjg0*#K9Dy70NVZlpxh4tAJYJU3ArAg z(fte{1)zh_(9l5W$N?Q49RvLE0T@Ym*w~m4@$m=<@bU2R2}vj(6B3aTP(UE000c(G_{$0fKn0Rj-_CsL!Vk%5-a-<0u#76Ndd=D%MC=U%Z4al%@SvN;LJ? zX>Egnn+C_TZVMO+Iu>_EqB&EIWpq;5Z1&1BYxY_eMNz2MG?(7bqbtKe5tKO$_eR4Q z@>gL{?AK$K9AaVRc&iR=IhK8DmfOTtDw>C%;6H1zNKy8;X=129{(efU|K&8(=6Slh zcI`1nV-y>Ir~R({jfR3>J%OvTsvC80(#aF%{WQr^)dltpzO*i-U+YU;<7fDzPu@Wx zu1j~Jxt#l4u8nxzQimy~gU&coFBzp%Iqt2?9-NgFY~XfZrVG(heQw zty`9NM!cA7t7;lsi4Z2zcolH8`ik#1hi_HkdCNg!UfW(@*GgxcCt zN-EeZ@mk@j7)Hk{g~mJtb$d#|Uaevn9>yI5w7PZG=fkZ5^pS~Kj$BxUx?2tF0y?G$ z&U?TmVDUCF_4hhvnV*khm%e8n41O#uI2a%piaE=RT55cP@o@>764<(+gi~>dokcx| z3$5N|0K|{=9fS$$&sr%0qfZ@-Y$N%IPb&q^PftAve{aIJ;RaN`9|r4sjCZxH>Nc-O z&mzl&7mLHX7+`rnIWuh7!Ggv(&k?R#ut?&wDz9yEC$>R0?-Q-O%3Hm%MdP?#hId-k znE@f8FHq71!MPue6IlK42sC-pnFNgn7cE{(krZpnhcdBM%?QU3catOVbkvO;b(4Sg zJC@-S<-fsygo0%A8?Aie46!DuUd#)*tf>Mk?EX=W0GW4-YEda8O?Bt!LiOHLkMCq6 zN8^rRFXVeepLoP@ilcbH^z~>qryxHCQeM4&tgfrolS~R{aK>xstrN5K&YAGu_AfTn zdXkVP`})z4U(&{FIHJJgz~0BsiTrWpIabjrTmROjqII@KB{_wXZ(?@E6o%-o5GhJmL%m~8VePP-l9F<_c@)OGuP-^*aFv347d?*GQ!Em*Fr1PFEAOi*OtxIg*AY?Qk_B02`hSY_CW@HC10&#l#gijF zDp2|&a86WI5TnhIkYF~<_R^?}SFmoONOS3w?YViPpmG4GS1zlq>Y!i5_UxGHx>ZAJ z?7HlW*cdvjxtQN~(aAF^lH1<&gWGk}F|VjA#)Q|VFKgH)eVd3?a65ln^hh4N zGe3SJiXmM1yMhvwdPttS1L|-7UB=v!e(lt#I3~nhJ~tavw7=O_!(fqpI_KDAV^&)F zcDB?DmRsLCTgkkVKpocBU6j)itzk+{u(|(ysNc5vS0%ANDS-fGhDaxI2D^)?9k*iM zeAZN>zl!aHQ1>rqbf8eDH`I^3X5jUOdF&92S;(DlcPlTr)} zcQoZD!&iGDeYqvdE!M2V#3`m~ob*s~ zR`R2r7bjT#y$Dt;%o1iA8Eou@9n^@l>nf@|ME#&jp??FJz`ViKgUh)XViJDFk7C)~ zx-1urv8SXus8zMrf&T7e$yd`3{Wk4Jw64M@s}PcPhla~A$|}`#3QK;wws)R;W20v! zPAaVx8pOOfoN-;*RE%=-tR49RS+?+cJHL&>#u*E)Rok{T+bsz6c>FBSf4G)h%0&NA zUGUAkjMO}}-S%0=QWGQliI-c2(0eJ~5T5QcihIDy|8wi}b053hPHv@Hhw<|$pWaQn zR}3C}lMq*l)-MbzNgLZ~P{ZoL>12%C85Jn(<3=Q=c{E?$s45K{3!q>qPU>3qxXW zt2U;m`-oqW$DR@7wRz)WH*bvWbwu6+ZCaFj=-GP;m&**0LWEmyAkq89)$bAK{U}PN zFGo8L6^DNj_N>3$9Fy}6-4^=qjd{v%1yl>w6u9psv(O|d-s(5Jg zqp&8X&UNOwP-~&aU2HK6!h-(qJfeyp7dUE@z3x)6t?BAf1Az=b6^?$fXmR!96x~T@ zj}aA(+Sj=dnbUB-Hs~;P!KEF0*6>X34NVvxOC3F8YyVXHVfG3PeDq<(b;X0Hyfw{n zJIn+^ROdUsTsX<0%Nwoq<28x4ExoR;TPZ9TUR*{|UW<}yXp8&yq;nf)#3|d0?Od~2 zKe*zM;xWU*;ryTz!{-Dlt>FXXj1F5!Pqfl&n%yYVb}6aTkx>JId9vug;$<5QTy{l+ zo!2bAhIJa9rQHbdwjzovg0eSS)lPZ4?Cz+wC%t&D;$xeYygF`%shVa#ne=INRH+PL z^jb|;Go>t@C44g|TJ}%vOpaso0Y3qI;ho~tI6#hSMnru#t=WxZWcxCfH~M;cpRad0 z2zk8h+?UDlc7gLY8dY`6qulJ?wzg0&1x$QzjYv>vH)QCaxtY-m(j@uh^PS4{7e`}D zmV_dD%j}9Tr(WaCx~ynDo3!-d;@_`61Gmc`=ejy6c#XJ6DzQ^!Y&Cq~z}#Xs@tzQG z*e5F)qno`mExD>O(vn4$A1Eo(Yu#eXMF#!Lc84^agV}1Y=Dy3q*v=9~pB?$eaD^M5 zm!EDXzDUok@ty8oi^+`kC{J;$V!D}ab3vc2o@L!lQbFV;9S%}Uj)dhU#oRO$DDYLE zmr+aSCS=xwtn_GtWrNhKsBM}#TD)sDHg6@gUyu0r(Whp9f&@Zu971Q}l3Gk-MC{I# zdoqluK6YMepSMBqk^^=4+(ZrREMO*^duBMVd3poJ%M*;pH?i9sr{|fm^l@4CqFm-0 zUwOPj%??vJdW+z=;7@c}qgKUQS@q(0fh8P9&uVK=Tl8j`Bfw=1jdOz@6T$9aY7Oda z!eOc#rnb`3No6&URU%Oxpckf=!TWLr($(8@$tx-!6O#Rg6QGgud_Fr&)W+Y!&X>k% z6qa>k(0ts!&D$uIH=^z3scIPnF;@OU5lx-cTw+*gZK!C^W|iiY2#pumS;$%cbwxAi z{+4^y$bZu?dh+=%Syi4sv<%JW2Ar8g2B+nUWP`pBezHAg@KjS4n_>$J`UpW##&UFx zdQG;`{)9o=rGCLuxJEi?!kC z;>wSX7J7riPSn&#Zji?>lqUpQP2cNGMfS>X_|_VdW=OXfT<(F~hL}$4Ya0^dwjgwN zpxTY=WELE5B>P)Gb|g5y?m-XvPDAoJu1L!F-4^#2T}Je?cZkf63Mn3wVO$X(Qz>@7 zkmep4BjiSkn3)PV9f9_ycdWq{WV=KaK0D~c3Ip$fiYvU&yPZ>o8yu@sya#-55lTm@ zQv!jBJlZ}jo4hL=Jni$$8(*T!h9@GetU7isEkAq(TObylWp~6idPYab2PPo>(kk&n zTbhdlLvs9hZ}d{t_nkG5;!I)^j12e4Iy?N6D^7Ih5>Q>{7sUx{^;aAFdKXiK(TwL z9=lNJEK%dzcfi`@7%6m^lNNA?m%!H_Hc0#nu7#%370|BUA#(j%=k3TQ9-*9*mrdAz zopF*5%#>15+v)^@yfz{AG|>@X;xckNG=|Cc{mw3)2{S*aVyvn@)F)?%zHx8-eyOiR z8PO*RvMWoBdoxr+61I?rJQ_wNG|IM&4G5sP_oQ=T`+##OiJ6@SMCE5!5xC~IFWbmS zA*h|(e8dma(p0#}L$|V2Enf|aOLXlurT6z@8&`a8E?6udX*b!q0#!t1JQ|C)cODw6 z#r))JA3F1hIgdO8-GQ9EnV`ZHv>p0-tq+n#eGr)UWfayYwZMsTqB+=wMZ5W!T+AV& zJHwSJS=Z@qsbEIculV?6^$Ay^&}i>3d=2`49iM((C5=fuQO;tMvV>6$oh8><;)gT` zxUt6m+P>aFvq&!9qiRn&p@ly@i_5vd+>FKPLq}Bcn2K z6>Hd$P}9(PT2L50JjnDjI+U3;k`l*h{209^f>}jmA16B4S?w;d^LOooT|d#V6t)bF zv3W}uo=>vUHr=)H%&PFg;JR z;!=L7x4JdxUA*RnzURmNI#x6(WTBet5${vt6ng;|Z|`sWg@tzebS|H^BbuRB{R1uu zf`d&vGj9WWh1@cP7N(^X=fJ&FZup`O-Wz2a_G8|ud&y}v{h%)8y^$4Y)~+$39haZv z$q)*xk@s7lYi1~y>I7y$8E&z6PdqA5Toz(%dR|Mus-YENOtA8vJnJ{T-56jXT` zFIGS5z1s;Vn+P#F;@|ju2dS`U6^S`n2vzp3o){#?-^A9@TK13+)Gq%6+yPXu7 zXcj}_E_wU5m3Q#xTDz<9ZfBc)LEK$X;CSB7ggT2rhrR1mVP^j23Okjie`UR}g?rN; z!QerLOnLW&dxGYW{3$Kl=GWX0vLXkv+dHB1lYZy?1LLE^w;XW?`}Y7{!Hs{_)A!Tr z*g>{zIv%1X9;)sTtmNd6!q-;33!g~7onxo`G=$d^@7`+6NXtkE|Gn?AN*1e&XqhI` z3%Kau`J4SB6-dG{cH5*|*|edRG4Go>hMBFmRY1!wh^pMY*lZFzPKSdXNAkSfl1`{8y|pctO@ zb7)CcEv`4R#OG?T9=BVqerG-Q9ym29`s}14l6X`l%0gyvu-k&}Ekc;RT?4g`oIis` zOfXoL@KKO1KQ#X3;kmk`ky?3FJ;27kYxYp*<^}RYCyw53D=!H_C5;$#Fa#P7#ud-1 zup$G>;$lFqm<9yL?SZDR7Y{~`)!)yUg_SqEq1-_5xEIO4+*!FAp0C?J_&4P9eW_J}63U#m;JDL>c9htYlO}3V#319dA zMhZUB;BQ9QPhY~ftO-Vyk1nxcstUac1&gx2gc%E%F}Y2Bega{6w3Dl#9I)%RiV3KS z;;3z3#AKz!PdVBr&Vds?vP}o}C00^;=NLM&?&AqDslFUXGjgaWi>pPY ze8W7)fxpEnvJWlZL6s}I@@dncSxL8(&TdO3`lB|M2Jjm09znUcuOMZT6w#KRV$(}`m7ttw;3)=>{&MaZxW*!vojKsxGos=*j|}gd@Q72 zOq;z%4;yt`g+lD*zqs3Dhw@3wz@Y!y6eop~+zf36g&8dLdcal%-?TT`5XHkM>{Xf# zpW&=;j-tQLCN;?W1Vj6bBmjzQW($Lj_;?b+-Q;dF^WU7!#U3^nuWY+rIXpg7^ozdU z(sZp@DB2Y=XY(#9u`;%#W@E6OtsT86&5T=i)~Zcw7!vP%v86pFIKIlkt6()>2&R}> z7C78Y9FSH0v7b@u7iemp->77V_+o{lY_oC^+RJb@sPlRmelkaan+89Ypl~Apbm6j7 zqH8og9$s~tN4g8v$Kk^IVx9f2sw0?k`BNm;V$wUIfqrfo4q^XI7KT?oOzSv$YGkL4 zv&zlGwsXuxW4xIMgpDt3T#S}I-oEpfIe;i0zn(5Oo@PQeDu_AZI$0YABd|*+Wb{CZG-_nl_40!5!w2!GrS)w{K%ob*>x>yc2cVbSJ z9&Zwb>2pi#WbcdxC8R975FPj)eWhTJpPNdX|1n}*#Fz-niCWbpjlwH@I=<|>FoE@A zj5~`8&6gmh=krs4AzRzo5p6bBLAj;Gy&6PHrTXT!2uY+NRj;1+OSc{q7#IpMsO3flq@bE&gvND$xwY-MmWj%pg|fsW4xOt|XcuV`?cWx-My= z@&D0gZMNzW7;f-uE|a${|7VJSRC$V}?0NixrIdm~O z{2T&q+kKvAhC1ltz$h##7cd~&UfNQJm0Se?8$zrVSTN1R`h-cCW}G3eWU=NTv8G;A zStQjXY3baz|B=_f6GQ?c0h*@~Pav;Ig9Wv}!Rb5$x0U67U{=t{w=nVXLw`EMr8T#u Q%MbAJQU5Cf_5IZU05zVYt^fc4 literal 0 HcmV?d00001 diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift index caebe8d305..72825c93c8 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Fixtures.swift @@ -21,6 +21,7 @@ internal enum Fixture: CaseIterable { case timePickersCountDown case timePickersWheels case timePickersCompact + case images var menuItemTitle: String { switch self { @@ -52,6 +53,8 @@ internal enum Fixture: CaseIterable { return "Time Picker (wheels)" case .timePickersCompact: return "Time Picker (compact)" + case .images: + return "Images" } } @@ -85,6 +88,8 @@ internal enum Fixture: CaseIterable { return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "TimePickersWheels") case .timePickersCompact: return UIStoryboard.datePickers.instantiateViewController(withIdentifier: "DatePickersCompact") // sharing the same VC with `datePickersCompact` + case .images: + return UIStoryboard.images.instantiateViewController(withIdentifier: "Images") } } } @@ -93,4 +98,5 @@ internal extension UIStoryboard { static var basic: UIStoryboard { UIStoryboard(name: "Basic", bundle: nil) } static var inputElements: UIStoryboard { UIStoryboard(name: "InputElements", bundle: nil) } static var datePickers: UIStoryboard { UIStoryboard(name: "InputElements-DatePickers", bundle: nil) } + static var images: UIStoryboard { UIStoryboard(name: "Images", bundle: nil) } } diff --git a/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard new file mode 100644 index 0000000000..c3e3bf4c9f --- /dev/null +++ b/DatadogSessionReplay/SRSnapshotTests/SRHost/Fixtures/Images.storyboard @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index f1841ccbe6..3ee20ed16e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 61E7DFB9299A5A3E001D7A3A /* Basic.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61E7DFB8299A5A3E001D7A3A /* Basic.storyboard */; }; 61E7DFBB299A5C9D001D7A3A /* BasicViewControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E7DFBA299A5C9D001D7A3A /* BasicViewControllers.swift */; }; 61EC1A37299CFD7E00224FB6 /* TestUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 61EC1A36299CFD7E00224FB6 /* TestUtilities */; }; + A797A95029D5958400EE73EB /* Images.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A797A94F29D5958400EE73EB /* Images.storyboard */; }; + A797A95229D59E7900EE73EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A797A95129D59E7900EE73EB /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -58,6 +60,8 @@ 61E7DFB6299A57A9001D7A3A /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; 61E7DFB8299A5A3E001D7A3A /* Basic.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Basic.storyboard; sourceTree = ""; }; 61E7DFBA299A5C9D001D7A3A /* BasicViewControllers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicViewControllers.swift; sourceTree = ""; }; + A797A94F29D5958400EE73EB /* Images.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Images.storyboard; sourceTree = ""; }; + A797A95129D59E7900EE73EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,6 +124,7 @@ 61B3BC502993BE2E0032C78A /* Main.storyboard */, 61B3BC552993BE2F0032C78A /* LaunchScreen.storyboard */, 61B3BC582993BE2F0032C78A /* Info.plist */, + A797A95129D59E7900EE73EB /* Assets.xcassets */, ); path = SRHost; sourceTree = ""; @@ -150,6 +155,7 @@ 616C37D9299F6913005E0472 /* InputElements.storyboard */, 61A735A929A5137400001820 /* InputViewControllers.swift */, 6196D32329AF7EB2002EACAF /* InputElements-DatePickers.storyboard */, + A797A94F29D5958400EE73EB /* Images.storyboard */, ); path = Fixtures; sourceTree = ""; @@ -249,6 +255,8 @@ 61B3BC572993BE2F0032C78A /* LaunchScreen.storyboard in Resources */, 61E7DFB9299A5A3E001D7A3A /* Basic.storyboard in Resources */, 61B3BC522993BE2E0032C78A /* Main.storyboard in Resources */, + A797A95229D59E7900EE73EB /* Assets.xcassets in Resources */, + A797A95029D5958400EE73EB /* Images.storyboard in Resources */, 6196D32429AF7EB2002EACAF /* InputElements-DatePickers.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/xcshareddata/xcschemes/SRSnapshotTests.xcscheme b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/xcshareddata/xcschemes/SRSnapshotTests.xcscheme index 6f5b3e5f44..ed43acc81e 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/xcshareddata/xcschemes/SRSnapshotTests.xcscheme +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/xcshareddata/xcschemes/SRSnapshotTests.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> @@ -38,6 +39,16 @@ ReferencedContainer = "container:SRSnapshotTests.xcodeproj"> + + + + BlueprintFrame.Content { - let base64Data = base64ImageString?.data(using: .utf8) - let imageData = Data(base64Encoded: base64Data!)! - - let contentType: BlueprintFrame.Content.ContentType = .image(image: UIImage(data: imageData, scale: UIScreen.main.scale)!) + let base64Data = base64ImageString?.data(using: .utf8) ?? Data() + let imageData = Data(base64Encoded: base64Data) ?? Data() + let image = UIImage(data: imageData, scale: UIScreen.main.scale) ?? UIImage() + let contentType: BlueprintFrame.Content.ContentType = .image(image: image) return .init(contentType: contentType) } From 0a2ffb9271fc7f80df5a695361360d2367b5c038 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 3 Apr 2023 12:40:50 +0200 Subject: [PATCH 68/72] Bumped version to 1.17.0 --- DatadogInternal.podspec | 2 +- DatadogLogs.podspec | 2 +- DatadogTrace.podspec | 2 +- TestUtilities.podspec | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index 08743c9f99..7295180831 100644 --- a/DatadogInternal.podspec +++ b/DatadogInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogInternal" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Datadog Internal Package. This module is not for public use." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index ff849f07a1..c107f964cd 100644 --- a/DatadogLogs.podspec +++ b/DatadogLogs.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogLogs" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Datadog Logs Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogTrace.podspec b/DatadogTrace.podspec index 44a348887a..dc921acf34 100644 --- a/DatadogTrace.podspec +++ b/DatadogTrace.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogTrace" - s.version = "1.16.0" + s.version = "1.17.0" s.summary = "Datadog Trace Module." s.homepage = "https://www.datadoghq.com" diff --git a/TestUtilities.podspec b/TestUtilities.podspec index 6977ba8584..12baeb2169 100644 --- a/TestUtilities.podspec +++ b/TestUtilities.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "TestUtilities" - s.version = "1.0.0" + s.version = "1.17.0" s.summary = "Datadog Testing Utilities. This module is for internal testing and should not be published." s.homepage = "https://www.datadoghq.com" From 759d185e89625ec6c240ea09975e20ca3d049c70 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 3 Apr 2023 15:36:47 +0200 Subject: [PATCH 69/72] REPLAY-1459 Session replay custom limits --- Datadog/Datadog.xcodeproj/project.pbxproj | 60 ++++++++++++------- DatadogInternal/Sources/DatadogFeature.swift | 7 +++ ...ft => FixedWidthInteger+Convenience.swift} | 0 ...e.swift => TimeInterval+Convenience.swift} | 0 .../PerformancePresetOverride.swift | 4 +- .../FixedWidthInteger+ConvenienceTests.swift | 4 +- .../TimeInterval+ConvenienceTests.swift | 2 +- .../Datadog/Core/PerformancePresetTests.swift | 1 + .../DatadogCore/DatadogCoreTests.swift | 19 +++--- 9 files changed, 58 insertions(+), 39 deletions(-) rename DatadogInternal/Sources/Extensions/{FixedWidthInteger+Convinience.swift => FixedWidthInteger+Convenience.swift} (100%) rename DatadogInternal/Sources/Extensions/{TimeInterval+Convinience.swift => TimeInterval+Convenience.swift} (100%) rename DatadogInternal/Sources/{ => Storage}/PerformancePresetOverride.swift (95%) rename Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift => DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.swift (92%) rename Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift => DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift (96%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 71f348ac71..ac0fb80b63 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -343,16 +343,6 @@ A728ADAC2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */; }; A728ADB02934EB0900397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; - A736BA2E29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */; }; - A736BA2F29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */; }; - A736BA3129D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */; }; - A736BA3229D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */; }; - A736BA3429D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */; }; - A736BA3529D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */; }; - A736BA3D29D1B7FE00C00966 /* TimeInterval+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */; }; - A736BA3E29D1B80200C00966 /* TimeInterval+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */; }; - A736BA3F29D1B80A00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */; }; - A736BA4029D1B80D00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A736BA3A29D1B7BA00C00966 /* FixedWidthInteger+ConvinienceTests.swift */; }; A79B0F64292BD074008742B3 /* DDOTelHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F63292BD074008742B3 /* DDOTelHTTPHeadersWriter+apiTests.m */; }; A79B0F65292BD074008742B3 /* DDOTelHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F63292BD074008742B3 /* DDOTelHTTPHeadersWriter+apiTests.m */; }; A79B0F66292BD7CA008742B3 /* OTelHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5E292BA435008742B3 /* OTelHTTPHeadersWriter+objc.swift */; }; @@ -454,6 +444,10 @@ D21C26D228A64599005DD405 /* MessageBusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26D028A64599005DD405 /* MessageBusTests.swift */; }; D21C26EE28AFB65B005DD405 /* ErrorMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */; }; D21C26EF28AFB65B005DD405 /* ErrorMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */; }; + D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; + D22F06D829DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; + D22F06D929DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */; }; + D22F06DA29DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */; }; D2303996298D50F1001A1FA3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; D2303997298D50F1001A1FA3 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* Datadog.framework */; }; D2303998298D50F1001A1FA3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; @@ -621,6 +615,12 @@ D25CFAA529C864E500E3A43D /* TracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */; }; D25EE93C29C4C3C300CE3839 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; platformFilter = ios; }; D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; + D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */; }; + D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */; }; + D263BCB429DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */; }; + D263BCB529DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */; }; + D263BCB629DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */; }; + D263BCB729DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */; }; D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; D26C49BF288982DA00802B2D /* FeatureUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49BE288982DA00802B2D /* FeatureUpload.swift */; }; @@ -1915,7 +1915,6 @@ A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersReaderTests.swift; sourceTree = ""; }; A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "W3CHTTPHeadersWriter+objc.swift"; sourceTree = ""; }; A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDW3CHTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; - A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePresetOverride.swift; sourceTree = ""; }; A736BA3029D1B6AF00C00966 /* TimeInterval+Convinience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Convinience.swift"; sourceTree = ""; }; A736BA3329D1B6E000C00966 /* FixedWidthInteger+Convinience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convinience.swift"; sourceTree = ""; }; A736BA3729D1B7B600C00966 /* TimeInterval+ConvinienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ConvinienceTests.swift"; sourceTree = ""; }; @@ -1970,6 +1969,8 @@ D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogMessageReceiverTests.swift; sourceTree = ""; }; D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageReceiverTests.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; + D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convenience.swift"; sourceTree = ""; }; + D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Convenience.swift"; sourceTree = ""; }; D23039A5298D513C001A1FA3 /* DatadogInternal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogInternal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D23039AD298D5234001A1FA3 /* DD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DD.swift; sourceTree = ""; }; D23039AF298D5235001A1FA3 /* Writer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Writer.swift; sourceTree = ""; }; @@ -2061,6 +2062,9 @@ D25CFAA129C8644E00E3A43D /* Casting+Tracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Casting+Tracing.swift"; sourceTree = ""; }; D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogTrace.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D25EE93B29C4C3C300CE3839 /* DatadogTraceTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogTraceTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformancePresetOverride.swift; sourceTree = ""; }; + D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+ConvenienceTests.swift"; sourceTree = ""; }; + D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ConvenienceTests.swift"; sourceTree = ""; }; D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisherTests.swift; sourceTree = ""; }; D26C49B52889416300802B2D /* UploadPerformancePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPerformancePreset.swift; sourceTree = ""; }; D26C49BE288982DA00802B2D /* FeatureUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureUpload.swift; sourceTree = ""; }; @@ -4099,6 +4103,7 @@ D23039AE298D5235001A1FA3 /* Storage */ = { isa = PBXGroup; children = ( + D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */, D23039AF298D5235001A1FA3 /* Writer.swift */, ); path = Storage; @@ -4182,6 +4187,8 @@ children = ( D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */, D23039D8298D5235001A1FA3 /* DatadogExtended.swift */, + D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */, + D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */, ); path = Extensions; sourceTree = ""; @@ -4355,6 +4362,15 @@ path = ../DatadogTrace/Tests; sourceTree = ""; }; + D263BCB129DB014900FA0E21 /* Extensions */ = { + isa = PBXGroup; + children = ( + D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */, + D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; D26C49B428893E5300802B2D /* Upload */ = { isa = PBXGroup; children = ( @@ -4483,7 +4499,7 @@ D2A783D829A530DC003B03BB /* MessageBus */, D2160CE729C0E00200FAA9A5 /* Swizzling */, D2EBEE3A29BA162900B15732 /* NetworkInstrumentation */, - A736BA2D29D1B16000C00966 /* PerformancePresetOverride.swift */, + D263BCB129DB014900FA0E21 /* Extensions */, ); name = DatadogInternalTests; path = ../DatadogInternal/Tests; @@ -5904,14 +5920,12 @@ 6141015B251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift in Sources */, D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */, 616CCE16250A467E009FED46 /* RUMInstrumentation.swift in Sources */, - A736BA2E29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */, 6157FA5E252767CB009A8A3B /* URLSessionRUMResourcesHandler.swift in Sources */, 616CCE13250A1868009FED46 /* RUMCommandSubscriber.swift in Sources */, 6112B11425C84E7900B37771 /* CrashReportSender.swift in Sources */, 6161247925CA9CA6009901BE /* CrashReporter.swift in Sources */, 615F197C25B5A64B00BE14B5 /* UIKitExtensions.swift in Sources */, 9EC8B5DA2668197B000F7529 /* VitalCPUReader.swift in Sources */, - A736BA3429D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */, 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */, 61B0385A2527247000518F3C /* DDURLSessionDelegate.swift in Sources */, 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */, @@ -5984,7 +5998,6 @@ 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, 614B0A4B24EBC43D00A2A780 /* RUMUserInfoProvider.swift in Sources */, - A736BA3129D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */, D2A1EE23287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6031,7 +6044,6 @@ 617B954224BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift in Sources */, 61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */, 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, - A736BA3F29D1B80A00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */, D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, 9EAF0CF6275A21100044E8CA /* WKUserContentController+DatadogTests.swift in Sources */, 61DA8CB2286215DE0074A606 /* CryptographyTests.swift in Sources */, @@ -6040,7 +6052,6 @@ 613E81F725A743600084B751 /* RUMEventsMapperTests.swift in Sources */, 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */, E143CCAF27D236F600F4018A /* CITestIntegrationTests.swift in Sources */, - A736BA3D29D1B7FE00C00966 /* TimeInterval+ConvinienceTests.swift in Sources */, D234613228B7713000055D4C /* FeatureContextTests.swift in Sources */, 615C3196251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift in Sources */, 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */, @@ -6357,6 +6368,7 @@ D23039F9298D5236001A1FA3 /* CoreLogger.swift in Sources */, D2160CA229C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, + D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, D2EBEE2629BA160F00B15732 /* OTelHTTPHeaders.swift in Sources */, @@ -6414,11 +6426,13 @@ D23039F2298D5236001A1FA3 /* AnyDecoder.swift in Sources */, D23039EF298D5236001A1FA3 /* FeatureMessage.swift in Sources */, D2160CA029C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, + D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D23039E5298D5236001A1FA3 /* DateProvider.swift in Sources */, D23039E0298D5235001A1FA3 /* DatadogCoreProtocol.swift in Sources */, D23039FD298D5236001A1FA3 /* DataCompression.swift in Sources */, D23039F0298D5236001A1FA3 /* AnyEncoder.swift in Sources */, D2A783D429A5309F003B03BB /* SwiftExtensions.swift in Sources */, + D22F06D929DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6672,13 +6686,11 @@ D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */, D2CB6E5A27C50EAE00A62B57 /* URLSessionAutoInstrumentation.swift in Sources */, D2CB6E5C27C50EAE00A62B57 /* UIKitRUMUserActionsHandler.swift in Sources */, - A736BA2F29D1B16000C00966 /* PerformancePresetOverride.swift in Sources */, D2CB6E5D27C50EAE00A62B57 /* RUMInstrumentation.swift in Sources */, D2CB6E5E27C50EAE00A62B57 /* URLSessionRUMResourcesHandler.swift in Sources */, D2CB6E5F27C50EAE00A62B57 /* RUMCommandSubscriber.swift in Sources */, D2CB6E6027C50EAE00A62B57 /* CrashReportSender.swift in Sources */, D2CB6E6127C50EAE00A62B57 /* CrashReporter.swift in Sources */, - A736BA3529D1B6E000C00966 /* FixedWidthInteger+Convinience.swift in Sources */, D2CB6E6327C50EAE00A62B57 /* UIKitExtensions.swift in Sources */, D2CB6E6427C50EAE00A62B57 /* VitalCPUReader.swift in Sources */, D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */, @@ -6750,7 +6762,6 @@ D2CB6EC427C50EAE00A62B57 /* DataUploadDelay.swift in Sources */, D2CB6EC727C50EAE00A62B57 /* PerformancePreset.swift in Sources */, D2CB6EC827C50EAE00A62B57 /* RUMUserInfoProvider.swift in Sources */, - A736BA3229D1B6AF00C00966 /* TimeInterval+Convinience.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6843,7 +6854,6 @@ 49274907288048B800ECD49B /* InternalTelemetryTests.swift in Sources */, D2CB6F2C27C520D400A62B57 /* JSONEncoderTests.swift in Sources */, D2CB6F2E27C520D400A62B57 /* RUMCommandTests.swift in Sources */, - A736BA4029D1B80D00C00966 /* FixedWidthInteger+ConvinienceTests.swift in Sources */, D2CB6F3027C520D400A62B57 /* DatadogExtensions.swift in Sources */, D2CB6F3227C520D400A62B57 /* JSONDataMatcher.swift in Sources */, D25085112976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, @@ -6875,7 +6885,6 @@ D2B3F053282E827B00C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, D2CB6F5827C520D400A62B57 /* RUMScopeTests.swift in Sources */, D20605BA2875729E0047275C /* ContextValuePublisherMock.swift in Sources */, - A736BA3E29D1B80200C00966 /* TimeInterval+ConvinienceTests.swift in Sources */, 615950F0291C05CD00470E0C /* SessionReplayDependencyTests.swift in Sources */, D2CB6F5B27C520D400A62B57 /* RUMEventSanitizerTests.swift in Sources */, D2CB6F5D27C520D400A62B57 /* UIKitRUMViewsPredicateTests.swift in Sources */, @@ -6985,6 +6994,7 @@ D2DA2358298D57AA00C6C7E6 /* CoreLogger.swift in Sources */, D2160CA329C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, + D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, D2EBEE3429BA161100B15732 /* OTelHTTPHeaders.swift in Sources */, @@ -7042,11 +7052,13 @@ D2DA2379298D57AA00C6C7E6 /* AnyDecoder.swift in Sources */, D2DA237A298D57AA00C6C7E6 /* FeatureMessage.swift in Sources */, D2160CA129C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, + D22F06D829DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D2DA237B298D57AA00C6C7E6 /* DateProvider.swift in Sources */, D2DA237C298D57AA00C6C7E6 /* DatadogCoreProtocol.swift in Sources */, D2DA237D298D57AA00C6C7E6 /* DataCompression.swift in Sources */, D2DA237E298D57AA00C6C7E6 /* AnyEncoder.swift in Sources */, D2A783D529A530A0003B03BB /* SwiftExtensions.swift in Sources */, + D22F06DA29DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7057,6 +7069,7 @@ D2160CD629C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */, D2EBEE3C29BA163E00B15732 /* OTelHTTPHeadersWriterTests.swift in Sources */, D2DA23A3298D58F400C6C7E6 /* AnyEncodableTests.swift in Sources */, + D263BCB429DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, D2EBEE4429BA168200B15732 /* TraceIDTests.swift in Sources */, D2EBEE4329BA168200B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23A7298D58F400C6C7E6 /* AppStateHistoryTests.swift in Sources */, @@ -7078,6 +7091,7 @@ D2DA23A9298D58F400C6C7E6 /* FeatureBaggageTests.swift in Sources */, D2A783DA29A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */, D2160CD429C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, + D263BCB629DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23AA298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, D2DA23A8298D58F400C6C7E6 /* DeviceInfoTests.swift in Sources */, ); @@ -7090,6 +7104,7 @@ D2160CD729C0DF6700FAA9A5 /* URLSessionSwizzlerTests.swift in Sources */, D2EBEE4029BA163F00B15732 /* OTelHTTPHeadersWriterTests.swift in Sources */, D2DA23B1298D59DC00C6C7E6 /* AnyEncodableTests.swift in Sources */, + D263BCB529DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, D2EBEE4629BA168400B15732 /* TraceIDTests.swift in Sources */, D2EBEE4529BA168400B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23B2298D59DC00C6C7E6 /* AppStateHistoryTests.swift in Sources */, @@ -7111,6 +7126,7 @@ D2DA23B7298D59DC00C6C7E6 /* FeatureBaggageTests.swift in Sources */, D2A783D929A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */, D2160CD529C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, + D263BCB729DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23B8298D59DC00C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, D2DA23BA298D59DC00C6C7E6 /* DeviceInfoTests.swift in Sources */, ); diff --git a/DatadogInternal/Sources/DatadogFeature.swift b/DatadogInternal/Sources/DatadogFeature.swift index 05d91e02d7..4dd40fed83 100644 --- a/DatadogInternal/Sources/DatadogFeature.swift +++ b/DatadogInternal/Sources/DatadogFeature.swift @@ -38,6 +38,9 @@ public protocol DatadogFeature { /// The `FeatureMessageReceiver` defines an interface for Feature to receive any message /// from a bus that is shared between Features registered in a core. var messageReceiver: FeatureMessageReceiver { get } + + /// (Optional) `PerformancePresetOverride` allows overriding certain performance presets if needed. + var performanceOverride: PerformancePresetOverride? { get } } @@ -52,3 +55,7 @@ public protocol DatadogRemoteFeature: DatadogFeature { /// The request will be transported by `DatadogCore`. var requestBuilder: FeatureRequestBuilder { get } } + +extension DatadogFeature { + public var performanceOverride: PerformancePresetOverride? { nil } +} diff --git a/DatadogInternal/Sources/Extensions/FixedWidthInteger+Convinience.swift b/DatadogInternal/Sources/Extensions/FixedWidthInteger+Convenience.swift similarity index 100% rename from DatadogInternal/Sources/Extensions/FixedWidthInteger+Convinience.swift rename to DatadogInternal/Sources/Extensions/FixedWidthInteger+Convenience.swift diff --git a/DatadogInternal/Sources/Extensions/TimeInterval+Convinience.swift b/DatadogInternal/Sources/Extensions/TimeInterval+Convenience.swift similarity index 100% rename from DatadogInternal/Sources/Extensions/TimeInterval+Convinience.swift rename to DatadogInternal/Sources/Extensions/TimeInterval+Convenience.swift diff --git a/DatadogInternal/Sources/PerformancePresetOverride.swift b/DatadogInternal/Sources/Storage/PerformancePresetOverride.swift similarity index 95% rename from DatadogInternal/Sources/PerformancePresetOverride.swift rename to DatadogInternal/Sources/Storage/PerformancePresetOverride.swift index 426f974fc3..b0fc584223 100644 --- a/DatadogInternal/Sources/PerformancePresetOverride.swift +++ b/DatadogInternal/Sources/Storage/PerformancePresetOverride.swift @@ -12,11 +12,11 @@ import Foundation public struct PerformancePresetOverride { /// An optional value representing the maximum allowed file size in bytes. /// If not provided, the default value from the `PerformancePreset` object is used. - let maxFileSize: UInt64? + public let maxFileSize: UInt64? /// An optional value representing the maximum allowed object size in bytes. /// If not provided, the default value from the `PerformancePreset` object is used. - let maxObjectSize: UInt64? + public let maxObjectSize: UInt64? /// Initializes a new `PerformancePresetOverride` instance with the provided /// maximum file size and maximum object size limits. diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift b/DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.swift similarity index 92% rename from Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift rename to DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.swift index 7ff3b71d07..25fa315150 100644 --- a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/FixedWidthInteger+ConvinienceTests.swift +++ b/DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.swift @@ -5,9 +5,9 @@ */ import XCTest -@testable import Datadog +@testable import DatadogInternal -final class FixedWidthIntegerConvinienceTests: XCTestCase { +final class FixedWidthIntegerConvenienceTests: XCTestCase { func test_Bytes() { let value: Int = 1_000 XCTAssertEqual(value.bytes, 1_000) diff --git a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift b/DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift similarity index 96% rename from Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift rename to DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift index ad7d1cbd3e..6588c79659 100644 --- a/Tests/DatadogTests/Datadog/DatadogInternal/Extensions/TimeInterval+ConvinienceTests.swift +++ b/DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import Datadog -final class TimeIntervalConvinienceTests: XCTestCase { +final class TimeIntervalConvenienceTests: XCTestCase { func test_Seconds() { XCTAssertEqual(TimeInterval(30).seconds, 30) XCTAssertEqual(Int(30).seconds, 30) diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index f761f9a789..43f016fc20 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -6,6 +6,7 @@ import XCTest import TestUtilities +import DatadogInternal @testable import Datadog class PerformancePresetTests: XCTestCase { diff --git a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift index e474f4d702..3a62f79572 100644 --- a/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -204,24 +204,19 @@ class DatadogCoreTests: XCTestCase { contextProvider: .mockAny(), applicationVersion: .mockAny() ) - let name = "mock" try core.register( - feature: FeatureMock( - name: name, - performanceOverride: nil - ) + feature: FeatureMock(performanceOverride: nil) ) - var feature = core.v2Features.values.first - XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, UInt64(512).KB) - XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, UInt64(4).MB) + var store = core.stores.values.first + XCTAssertEqual(store?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, UInt64(512).KB) + XCTAssertEqual(store?.storage.authorizedFilesOrchestrator.performance.maxFileSize, UInt64(4).MB) try core.register( feature: FeatureMock( - name: name, performanceOverride: PerformancePresetOverride(maxFileSize: 123, maxObjectSize: 456) ) ) - feature = core.v2Features.values.first - XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, 456) - XCTAssertEqual(feature?.storage.authorizedFilesOrchestrator.performance.maxFileSize, 123) + store = core.stores.values.first + XCTAssertEqual(store?.storage.authorizedFilesOrchestrator.performance.maxObjectSize, 456) + XCTAssertEqual(store?.storage.authorizedFilesOrchestrator.performance.maxFileSize, 123) } } From d6bc4732a47be0e67a89024ac6051a1015feb6d6 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 3 Apr 2023 15:42:11 +0200 Subject: [PATCH 70/72] RUMM-2997 Send a crash to both RUM and Logging features --- .../DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 763b28689a..06d704c8cb 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -6,6 +6,8 @@ import TestUtilities import DatadogInternal + +@testable import DatadogLogs @testable import Datadog extension CrashReporter { From df307bfe6db17e5b6a3db147cde54fb1df563419 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Mon, 3 Apr 2023 15:43:05 +0200 Subject: [PATCH 71/72] RUMM-2872 Allow users to manually end a RUM session --- Datadog/Datadog.xcodeproj/project.pbxproj | 21 -------------- .../RUM/RUMStopSessionScenarioTests.swift | 10 +++---- .../project.pbxproj | 28 +++++++++++++++++++ .../KioskSendEventsViewController.swift | 0 ...kSendInterruptedEventsViewController.swift | 0 .../KioskViewController.swift | 0 .../RUMStopSessionScenario.storyboard | 18 ++++++------ 7 files changed, 42 insertions(+), 35 deletions(-) rename {Datadog/Example => IntegrationTests/Runner}/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift (100%) rename {Datadog/Example => IntegrationTests/Runner}/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift (100%) rename {Datadog/Example => IntegrationTests/Runner}/Scenarios/RUM/StopSessionScenario/KioskViewController.swift (100%) rename {Datadog/Example => IntegrationTests/Runner}/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard (94%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index ac0fb80b63..ab0fbee436 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -7,11 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 490D5EC929C9E17E004F969C /* RUMStopSessionScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */; }; - 490D5ECF29CA0745004F969C /* RUMStopSessionScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */; }; - 490D5ED029CA074A004F969C /* KioskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ECD29CA0738004F969C /* KioskViewController.swift */; }; - 490D5ED329CA08F7004F969C /* KioskSendEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */; }; - 490D5ED629CA1DD6004F969C /* KioskSendInterruptedEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */; }; 49274906288048B500ECD49B /* InternalTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */; }; 49274907288048B800ECD49B /* InternalTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */; }; 4927490B288048FF00ECD49B /* RUMInternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */; }; @@ -1517,11 +1512,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 490D5EC829C9E17E004F969C /* RUMStopSessionScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMStopSessionScenarioTests.swift; sourceTree = ""; }; - 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMStopSessionScenario.storyboard; sourceTree = ""; }; - 490D5ECD29CA0738004F969C /* KioskViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskViewController.swift; sourceTree = ""; }; - 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSendEventsViewController.swift; sourceTree = ""; }; - 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSendInterruptedEventsViewController.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalTelemetryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalTelemetryTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; 61020C292757AD91005EEAEA /* BackgroundLocationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundLocationMonitor.swift; sourceTree = ""; }; @@ -2436,17 +2426,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 490D5ECA29C9E28F004F969C /* StopSessionScenario */ = { - isa = PBXGroup; - children = ( - 490D5ECB29C9E2D0004F969C /* RUMStopSessionScenario.storyboard */, - 490D5ECD29CA0738004F969C /* KioskViewController.swift */, - 490D5ED129CA087D004F969C /* KioskSendEventsViewController.swift */, - 490D5ED429CA1D9F004F969C /* KioskSendInterruptedEventsViewController.swift */, - ); - path = StopSessionScenario; - sourceTree = ""; - }; 61020C272757AD63005EEAEA /* BackgroundEvents */ = { isa = PBXGroup; children = ( diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMStopSessionScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMStopSessionScenarioTests.swift index ff83cffeb3..8a81505aec 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMStopSessionScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMStopSessionScenarioTests.swift @@ -83,7 +83,7 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { let view1 = appStartSession.viewVisits[0] XCTAssertEqual(view1.name, "KioskViewController") - XCTAssertEqual(view1.path, "Example.KioskViewController") + XCTAssertEqual(view1.path, "Runner.KioskViewController") XCTAssertEqual(view1.viewEvents.last?.session.isActive, false) RUMSessionMatcher.assertViewWasEventuallyInactive(view1) } @@ -96,7 +96,7 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { let view1 = normalSession.viewVisits[0] XCTAssertTrue(try XCTUnwrap(view1.viewEvents.first?.session.isActive)) XCTAssertEqual(view1.name, "KioskSendEvents") - XCTAssertEqual(view1.path, "Example.KioskSendEventsViewController") + XCTAssertEqual(view1.path, "Runner.KioskSendEventsViewController") XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) @@ -106,7 +106,7 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { let view2 = normalSession.viewVisits[1] XCTAssertEqual(view2.name, "KioskViewController") - XCTAssertEqual(view2.path, "Example.KioskViewController") + XCTAssertEqual(view2.path, "Runner.KioskViewController") XCTAssertEqual(view2.viewEvents.last?.session.isActive, false) RUMSessionMatcher.assertViewWasEventuallyInactive(view2) } @@ -119,7 +119,7 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { let view1 = interruptedSession.viewVisits[0] XCTAssertTrue(try XCTUnwrap(view1.viewEvents.first?.session.isActive)) XCTAssertEqual(view1.name, "KioskSendInterruptedEvents") - XCTAssertEqual(view1.path, "Example.KioskSendInterruptedEventsViewController") + XCTAssertEqual(view1.path, "Runner.KioskSendInterruptedEventsViewController") XCTAssertEqual(view1.resourceEvents[0].resource.url, "https://foo.com/resource/1") XCTAssertEqual(view1.resourceEvents[0].resource.statusCode, 200) XCTAssertEqual(view1.resourceEvents[0].resource.type, .image) @@ -129,7 +129,7 @@ class RUMStopSessionScenarioTests: IntegrationTests, RUMCommonAsserts { let view2 = interruptedSession.viewVisits[1] XCTAssertEqual(view2.name, "KioskViewController") - XCTAssertEqual(view2.path, "Example.KioskViewController") + XCTAssertEqual(view2.path, "Runner.KioskViewController") XCTAssertEqual(view2.viewEvents.last?.session.isActive, false) RUMSessionMatcher.assertViewWasEventuallyInactive(view2) } diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj index 96d57e7e0f..24f8b6a3b6 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj @@ -88,7 +88,12 @@ 9EC2835E26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9EC2835D26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard */; }; 9EC2836026CFF59400FACF1C /* RUMMobileVitalsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2835F26CFF59400FACF1C /* RUMMobileVitalsViewController.swift */; }; D218665029967CBC006F5B23 /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D218664E29967CBC006F5B23 /* RUMDataModels.swift */; }; + D22F06CF29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06CB29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift */; }; + D22F06D029DAE5360026CC3C /* KioskSendEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06CC29DAE5360026CC3C /* KioskSendEventsViewController.swift */; }; + D22F06D129DAE5360026CC3C /* KioskViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06CD29DAE5360026CC3C /* KioskViewController.swift */; }; + D22F06D229DAE5360026CC3C /* RUMStopSessionScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D22F06CE29DAE5360026CC3C /* RUMStopSessionScenario.storyboard */; }; D240688627CFA64A00C04F44 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D240688427CFA64A00C04F44 /* LaunchScreen.storyboard */; }; + D263BCB829DB057E00FA0E21 /* RUMStopSessionScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D329DAE59A0026CC3C /* RUMStopSessionScenarioTests.swift */; }; D2774EE4299E2E90004EC36A /* RUMSessionMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2774EDF299E2E90004EC36A /* RUMSessionMatcher.swift */; }; D2774EE5299E2E90004EC36A /* RUMEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2774EE0299E2E90004EC36A /* RUMEventMatcher.swift */; }; D2774EE6299E2E90004EC36A /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2774EE1299E2E90004EC36A /* SpanMatcher.swift */; }; @@ -217,6 +222,11 @@ D223CA4F29966B18000CEDBF /* Runner.release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Runner.release.xcconfig; sourceTree = ""; }; D223CA5029966B19000CEDBF /* Runner.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Runner.debug.xcconfig; sourceTree = ""; }; D223CA5129966B19000CEDBF /* Runner.integration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Runner.integration.xcconfig; sourceTree = ""; }; + D22F06CB29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KioskSendInterruptedEventsViewController.swift; sourceTree = ""; }; + D22F06CC29DAE5360026CC3C /* KioskSendEventsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KioskSendEventsViewController.swift; sourceTree = ""; }; + D22F06CD29DAE5360026CC3C /* KioskViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KioskViewController.swift; sourceTree = ""; }; + D22F06CE29DAE5360026CC3C /* RUMStopSessionScenario.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RUMStopSessionScenario.storyboard; sourceTree = ""; }; + D22F06D329DAE59A0026CC3C /* RUMStopSessionScenarioTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMStopSessionScenarioTests.swift; sourceTree = ""; }; D240688527CFA64A00C04F44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D2774EDF299E2E90004EC36A /* RUMSessionMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMSessionMatcher.swift; sourceTree = ""; }; D2774EE0299E2E90004EC36A /* RUMEventMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMEventMatcher.swift; sourceTree = ""; }; @@ -404,6 +414,7 @@ 6193DCA2251B5669009B8011 /* TapActionAutoInstrumentation */, D2791EF32716F16E0046E07A /* SwiftUIInstrumentation */, 612D8F6725AEE65F000E2E09 /* Scrubbing */, + D22F06CA29DAE5360026CC3C /* StopSessionScenario */, ); path = RUM; sourceTree = ""; @@ -593,6 +604,7 @@ 612D8F8025AF1C74000E2E09 /* RUMScrubbingScenarioTests.swift */, 9EC2835926CFEE0B00FACF1C /* RUMMobileVitalsScenarioTests.swift */, D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */, + D22F06D329DAE59A0026CC3C /* RUMStopSessionScenarioTests.swift */, ); path = RUM; sourceTree = ""; @@ -643,6 +655,17 @@ path = ../../Sources/Datadog/RUM/DataModels; sourceTree = ""; }; + D22F06CA29DAE5360026CC3C /* StopSessionScenario */ = { + isa = PBXGroup; + children = ( + D22F06CB29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift */, + D22F06CC29DAE5360026CC3C /* KioskSendEventsViewController.swift */, + D22F06CD29DAE5360026CC3C /* KioskViewController.swift */, + D22F06CE29DAE5360026CC3C /* RUMStopSessionScenario.storyboard */, + ); + path = StopSessionScenario; + sourceTree = ""; + }; D2774EDE299E2E90004EC36A /* Matchers */ = { isa = PBXGroup; children = ( @@ -794,6 +817,7 @@ 61337039250F852E00236D58 /* RUMManualInstrumentationScenario.storyboard in Resources */, 6193DCA4251B5691009B8011 /* RUMTapActionScenario.storyboard in Resources */, 6167ACC7251A0BCE0012B4D0 /* NSURLSessionScenario.storyboard in Resources */, + D22F06D229DAE5360026CC3C /* RUMStopSessionScenario.storyboard in Resources */, 6111543625C993C2007C84C9 /* CrashReportingScenario.storyboard in Resources */, D240688627CFA64A00C04F44 /* LaunchScreen.storyboard in Resources */, 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */, @@ -869,6 +893,7 @@ files = ( D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */, 618DCFE124C766F500589570 /* SendRUMFixture2ViewController.swift in Sources */, + D22F06CF29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift in Sources */, 9EA95C1E2791C9BE00F6C1F3 /* WebViewScenarios.swift in Sources */, 61F9CA8025125C01000A5E61 /* RUMNCSScreen3ViewController.swift in Sources */, 6164AE89252B4ECA000D78C4 /* SendThirdPartyRequestsViewController.swift in Sources */, @@ -877,10 +902,12 @@ 61163C37252DDD60007DD5BF /* RUMMVSViewController.swift in Sources */, 61D50C5A2580EFF3006038A3 /* URLSessionScenarios.swift in Sources */, 614CADCE250FCA0200B93D2D /* TestScenarios.swift in Sources */, + D22F06D029DAE5360026CC3C /* KioskSendEventsViewController.swift in Sources */, 6111542525C992F8007C84C9 /* CrashReportingScenarios.swift in Sources */, 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */, 61098D2B27FEE3F00021237A /* MessagePortChannel.swift in Sources */, 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */, + D22F06D129DAE5360026CC3C /* KioskViewController.swift in Sources */, 6111543F25C996A5007C84C9 /* CrashReportingViewController.swift in Sources */, 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */, 61D50C542580EF41006038A3 /* RUMScenarios.swift in Sources */, @@ -944,6 +971,7 @@ 6164AF2E252C9C51000D78C4 /* RUMResourcesScenarioTests.swift in Sources */, D2774EE8299E2E90004EC36A /* JSONDataMatcher.swift in Sources */, 615AAC07251E217B00C89EE9 /* RUMTapActionScenarioTests.swift in Sources */, + D263BCB829DB057E00FA0E21 /* RUMStopSessionScenarioTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift b/IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift similarity index 100% rename from Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift rename to IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskSendEventsViewController.swift diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift b/IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift similarity index 100% rename from Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift rename to IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskSendInterruptedEventsViewController.swift diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.swift b/IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskViewController.swift similarity index 100% rename from Datadog/Example/Scenarios/RUM/StopSessionScenario/KioskViewController.swift rename to IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/KioskViewController.swift diff --git a/Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard b/IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard similarity index 94% rename from Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard rename to IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard index 1d3add9c1f..27aabc57cc 100644 --- a/Datadog/Example/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard +++ b/IntegrationTests/Runner/Scenarios/RUM/StopSessionScenario/RUMStopSessionScenario.storyboard @@ -28,7 +28,7 @@ - + @@ -46,19 +46,19 @@