From 127f61cec03b2766e39fdb5c6222142d56e079a3 Mon Sep 17 00:00:00 2001 From: "Xavier F. Gouchet" Date: Thu, 21 Dec 2023 09:04:02 +0100 Subject: [PATCH 01/60] RUM-2442 print RUM info for Synthetics --- .../Sources/RUMMonitor/Scopes/RUMApplicationScope.swift | 5 +++++ DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift | 5 +++++ DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift index 62f43f8bd5..d73cf8a1cb 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -43,6 +43,11 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { activeViewName: nil, activeUserActionID: nil ) + + // Notify Synthetics if needed + if dependencies.syntheticsTest != nil { + print("_dd.application.id=" + dependencies.rumApplicationID) + } } // MARK: - RUMContextProvider diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index 4bc48f9493..1ce23fc938 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -121,6 +121,11 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Update `CrashContext` with recent RUM session state: dependencies.core?.send(message: .baggage(key: RUMBaggageKeys.sessionState, value: state)) + + // Notify Synthetics if needed + if dependencies.syntheticsTest != nil && self.sessionUUID != .nullUUID { + print("_dd.session.id=" + self.sessionUUID.toRUMDataFormat) + } } /// Creates a new Session upon expiration of the previous one. diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index fc635b52b6..447bf0ebc3 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -124,6 +124,11 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { frequency: $0.frequency ) } + + // Notify Synthetics if needed + if dependencies.syntheticsTest != nil && self.context.sessionID != .nullUUID { + print("_dd.view.id=" + self.viewUUID.toRUMDataFormat) + } } // MARK: - RUMContextProvider From b6946a9f1a304b7a028e74e372aecd604eb0449e Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 29 Nov 2023 17:13:57 +0000 Subject: [PATCH 02/60] RUM-2153 Rename --- Datadog/Datadog.xcodeproj/project.pbxproj | 8 ++++---- .../Sources/Feature/SessionReplayFeature.swift | 4 ++-- .../{Processor.swift => SnapshotProcessor.swift} | 4 ++-- DatadogSessionReplay/Sources/Recorder/Recorder.swift | 6 +++--- DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift | 2 +- .../Tests/Processor/ProcessorTests.swift | 12 ++++++------ 6 files changed, 18 insertions(+), 18 deletions(-) rename DatadogSessionReplay/Sources/Processor/{Processor.swift => SnapshotProcessor.swift} (98%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 703a35735f..a22de20f6b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -125,7 +125,7 @@ 61054E922A6EE10A00AAA894 /* SegmentJSONBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E452A6EE10A00AAA894 /* SegmentJSONBuilder.swift */; }; 61054E932A6EE10A00AAA894 /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E472A6EE10A00AAA894 /* MultipartFormData.swift */; }; 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4A2A6EE10A00AAA894 /* TextObfuscator.swift */; }; - 61054E952A6EE10A00AAA894 /* Processor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4B2A6EE10A00AAA894 /* Processor.swift */; }; + 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */; }; 61054E962A6EE10A00AAA894 /* Diff+SRWireframes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4D2A6EE10A00AAA894 /* Diff+SRWireframes.swift */; }; 61054E972A6EE10A00AAA894 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4E2A6EE10A00AAA894 /* Diff.swift */; }; 61054E982A6EE10A00AAA894 /* RecordsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */; }; @@ -1955,7 +1955,7 @@ 61054E452A6EE10A00AAA894 /* SegmentJSONBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentJSONBuilder.swift; sourceTree = ""; }; 61054E472A6EE10A00AAA894 /* MultipartFormData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = ""; }; 61054E4A2A6EE10A00AAA894 /* TextObfuscator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextObfuscator.swift; sourceTree = ""; }; - 61054E4B2A6EE10A00AAA894 /* Processor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Processor.swift; sourceTree = ""; }; + 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessor.swift; sourceTree = ""; }; 61054E4D2A6EE10A00AAA894 /* Diff+SRWireframes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Diff+SRWireframes.swift"; sourceTree = ""; }; 61054E4E2A6EE10A00AAA894 /* Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diff.swift; sourceTree = ""; }; 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordsBuilder.swift; sourceTree = ""; }; @@ -3317,7 +3317,7 @@ isa = PBXGroup; children = ( 61054E492A6EE10A00AAA894 /* Privacy */, - 61054E4B2A6EE10A00AAA894 /* Processor.swift */, + 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */, 61054E4C2A6EE10A00AAA894 /* Diffing */, 61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */, 61054E522A6EE10A00AAA894 /* Flattening */, @@ -7690,7 +7690,7 @@ 61054E6E2A6EE10A00AAA894 /* RecordingCoordinator.swift in Sources */, 61054E9F2A6EE10B00AAA894 /* Errors.swift in Sources */, 61054E642A6EE10A00AAA894 /* SessionReplay.swift in Sources */, - 61054E952A6EE10A00AAA894 /* Processor.swift in Sources */, + 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */, 61054E722A6EE10A00AAA894 /* TouchIdentifierGenerator.swift in Sources */, 61054E912A6EE10A00AAA894 /* EnrichedRecordJSON.swift in Sources */, 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index e0d05040e9..e6f232ee32 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -19,7 +19,7 @@ internal class SessionReplayFeature: DatadogRemoteFeature { /// Orchestrates the process of capturing next snapshots on the main thread. let recordingCoordinator: RecordingCoordinator /// Processes each new snapshot on a background thread and transforms it into records. - let processor: Processing + let processor: SnapshotProcessing // MARK: - Initialization @@ -27,7 +27,7 @@ internal class SessionReplayFeature: DatadogRemoteFeature { core: DatadogCoreProtocol, configuration: SessionReplay.Configuration ) throws { - let processor = Processor( + let processor = SnapshotProcessor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.processor"), writer: RecordWriter(core: core), srContextPublisher: SRContextPublisher(core: core), diff --git a/DatadogSessionReplay/Sources/Processor/Processor.swift b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift similarity index 98% rename from DatadogSessionReplay/Sources/Processor/Processor.swift rename to DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift index 2a1b03803e..768e36a8ca 100644 --- a/DatadogSessionReplay/Sources/Processor/Processor.swift +++ b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift @@ -13,7 +13,7 @@ import DatadogInternal /// This is the actual brain of Session Replay. Based on the sequence of snapshots it receives, it computes the sequence /// of records that will to be send to SR BE. It implements the logic of reducing snapshots into Full or Incremental /// mutation records. -internal protocol Processing { +internal protocol SnapshotProcessing { /// Accepts next view-tree and touch snapshots. /// - Parameter viewTreeSnapshot: the snapshot of a next view tree /// - Parameter touchSnapshot: the snapshot of next touch interactions (or `nil` if no interactions happened) @@ -30,7 +30,7 @@ internal protocol Processing { /// - the array of wireframes is attached to SR record (see `RecordsBuidler`); /// - succeeding records are enriched with their RUM context and written to `DatadogCore`; /// - when `DatadogCore` triggers an upload, batched records are deserialized, grouped into SR segments and then uploaded. -internal class Processor: Processing { +internal class SnapshotProcessor: SnapshotProcessing { /// Flattens VTS received from `Recorder` by removing invisible nodes. private let nodesFlattener = NodesFlattener() /// Builds SR wireframes to describe UI elements. diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index 7df68f2386..5fd0944227 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -59,12 +59,12 @@ public class Recorder: Recording { /// Captures touch snapshot. private let touchSnapshotProducer: TouchSnapshotProducer /// Turns view tree snapshots into data models that will be uploaded to SR BE. - private let snapshotProcessor: Processing + private let snapshotProcessor: SnapshotProcessing /// Sends telemetry through sdk core. private let telemetry: Telemetry convenience init( - processor: Processing, + processor: SnapshotProcessing, telemetry: Telemetry, additionalNodeRecorders: [NodeRecorder] ) throws { @@ -90,7 +90,7 @@ public class Recorder: Recording { uiApplicationSwizzler: UIApplicationSwizzler, viewTreeSnapshotProducer: ViewTreeSnapshotProducer, touchSnapshotProducer: TouchSnapshotProducer, - snapshotProcessor: Processing, + snapshotProcessor: SnapshotProcessing, telemetry: Telemetry ) { self.uiApplicationSwizzler = uiApplicationSwizzler diff --git a/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift index 9d1e202526..9f52d9daa0 100644 --- a/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift @@ -8,7 +8,7 @@ import Foundation @testable import DatadogSessionReplay /// Spies the interaction with `Processing`. -internal class ProcessorSpy: Processing { +internal class ProcessorSpy: SnapshotProcessing { /// An array of snapshots recorded in `process(viewTreeSnapshot:touchSnapshot:)` private(set) var processedSnapshots: [(viewTreeSnapshot: ViewTreeSnapshot, touchSnapshot: TouchSnapshot?)] = [] diff --git a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift index f8faca5fc1..ddb462c206 100644 --- a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift @@ -29,7 +29,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When @@ -61,7 +61,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When @@ -108,7 +108,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let view = UIView.mock(withFixture: .visible(.someAppearance)) view.frame = CGRect(x: 0, y: 0, width: 100, height: 200) let rotatedView = UIView.mock(withFixture: .visible(.someAppearance)) @@ -147,7 +147,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When @@ -201,7 +201,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) // When let touchSnapshot = generateTouchSnapshot(startAt: earliestTouchTime, endAt: snapshotTime, numberOfTouches: numberOfTouches) @@ -246,7 +246,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = Processor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When From be19fdaaa54b1506ab5162c8260f89aacb467524 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 11 Dec 2023 15:25:08 +0000 Subject: [PATCH 03/60] RUM-2153 Add resources recording capablity --- Datadog/Datadog.xcodeproj/project.pbxproj | 55 ++++++++++--------- .../Feature/SessionReplayFeature.swift | 19 ++++--- .../Sources/Models/EnrichedResource.swift | 6 +- .../Processor/ResourcesProcessor.swift | 35 ++++++++++++ .../Sources/Processor/SnapshotProcessor.swift | 8 +-- .../Sources/Recorder/Recorder.swift | 17 +++++- .../Utilities/ImageDataProvider.swift | 24 ++++++-- .../NodeRecorders/UIDatePickerRecorder.swift | 28 +++++----- .../NodeRecorders/UIImageViewRecorder.swift | 24 +++++++- .../NodeRecorders/UIPickerViewRecorder.swift | 16 ++++-- .../NodeRecorders/UITextFieldRecorder.swift | 7 ++- .../NodeRecorders/UIViewRecorder.swift | 2 +- .../ViewTreeSnapshot/ViewTreeRecorder.swift | 24 ++++++-- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 17 +++++- .../ViewTreeSnapshotBuilder.swift | 4 +- .../Sources/Writers/ResourcesWriter.swift | 8 +-- .../Tests/Mocks/RecorderMocks.swift | 42 ++++++++++++-- .../ViewTreeRecorderTests.swift | 27 ++++----- 18 files changed, 266 insertions(+), 97 deletions(-) create mode 100644 DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index a22de20f6b..e095250c24 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -501,10 +501,6 @@ A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A71013D62B178FAD00101E60 /* ResourcesWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */; }; A71265862B17980C007D63CE /* MockFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71265852B17980C007D63CE /* MockFeature.swift */; }; - A712658F2B179C94007D63CE /* EnrichedResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A712658B2B179C93007D63CE /* EnrichedResource.swift */; }; - A71265902B179C94007D63CE /* SRDataModels+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A712658C2B179C93007D63CE /* SRDataModels+UIKit.swift */; }; - A71265912B179C94007D63CE /* SRDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A712658D2B179C93007D63CE /* SRDataModels.swift */; }; - A71265922B179C94007D63CE /* EnrichedRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = A712658E2B179C93007D63CE /* EnrichedRecord.swift */; }; A728ADAB2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */; }; 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 */; }; @@ -518,6 +514,11 @@ A79B0F65292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */; }; A79B0F66292BD7CA008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */; }; A79B0F67292BD7CC008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */; }; + A7B932F52B1F694000AE6477 /* ResourcesProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */; }; + A7B932FB2B1F6A0A00AE6477 /* EnrichedRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */; }; + A7B932FC2B1F6A0A00AE6477 /* SRDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */; }; + A7B932FD2B1F6A0A00AE6477 /* EnrichedResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */; }; + A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */; }; A7C816AB2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; A7C816AC2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; A7DA18042AB0C91200F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; @@ -2424,10 +2425,6 @@ A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskCoordinator.swift; sourceTree = ""; }; A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriterTests.swift; sourceTree = ""; }; A71265852B17980C007D63CE /* MockFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeature.swift; sourceTree = ""; }; - A712658B2B179C93007D63CE /* EnrichedResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnrichedResource.swift; path = ../../Models/EnrichedResource.swift; sourceTree = ""; }; - A712658C2B179C93007D63CE /* SRDataModels+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SRDataModels+UIKit.swift"; path = "../../Models/SRDataModels+UIKit.swift"; sourceTree = ""; }; - A712658D2B179C93007D63CE /* SRDataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SRDataModels.swift; path = ../../Models/SRDataModels.swift; sourceTree = ""; }; - A712658E2B179C93007D63CE /* EnrichedRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnrichedRecord.swift; path = ../../Models/EnrichedRecord.swift; sourceTree = ""; }; A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeaders.swift; sourceTree = ""; }; A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersWriter.swift; sourceTree = ""; }; A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersReader.swift; sourceTree = ""; }; @@ -2444,6 +2441,11 @@ A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "B3HTTPHeadersWriter+objc.swift"; sourceTree = ""; }; A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersReaderTests.swift; sourceTree = ""; }; A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDB3HTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; + A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesProcessor.swift; sourceTree = ""; }; + A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedRecord.swift; sourceTree = ""; }; + A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRDataModels.swift; sourceTree = ""; }; + A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedResource.swift; sourceTree = ""; }; + A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SRDataModels+UIKit.swift"; sourceTree = ""; }; A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMActionsPredicateTests.swift; sourceTree = ""; }; @@ -3124,7 +3126,7 @@ 61054E0C2A6EE10A00AAA894 /* SessionReplay.swift */, 61054E0B2A6EE10A00AAA894 /* SessionReplayConfiguration.swift */, 61054E542A6EE10A00AAA894 /* Utilities */, - 61054E042A6EE10A00AAA894 /* Models */, + A7B932F62B1F6A0A00AE6477 /* Models */, 61054E032A6EE10A00AAA894 /* Writers */, ); name = DatadogSessionReplay; @@ -3158,18 +3160,6 @@ path = Writers; sourceTree = ""; }; - 61054E042A6EE10A00AAA894 /* Models */ = { - isa = PBXGroup; - children = ( - A712658E2B179C93007D63CE /* EnrichedRecord.swift */, - A712658B2B179C93007D63CE /* EnrichedResource.swift */, - A712658D2B179C93007D63CE /* SRDataModels.swift */, - A712658C2B179C93007D63CE /* SRDataModels+UIKit.swift */, - ); - name = Models; - path = Writers/Models; - sourceTree = ""; - }; 61054E0D2A6EE10A00AAA894 /* Recorder */ = { isa = PBXGroup; children = ( @@ -3316,8 +3306,9 @@ 61054E482A6EE10A00AAA894 /* Processor */ = { isa = PBXGroup; children = ( - 61054E492A6EE10A00AAA894 /* Privacy */, 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */, + A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */, + 61054E492A6EE10A00AAA894 /* Privacy */, 61054E4C2A6EE10A00AAA894 /* Diffing */, 61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */, 61054E522A6EE10A00AAA894 /* Flattening */, @@ -5092,6 +5083,17 @@ path = W3C; sourceTree = ""; }; + A7B932F62B1F6A0A00AE6477 /* Models */ = { + isa = PBXGroup; + children = ( + A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */, + A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */, + A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */, + A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */, + ); + path = Models; + sourceTree = ""; + }; A7F773D929253F5900AC1A62 /* Datadog */ = { isa = PBXGroup; children = ( @@ -7644,7 +7646,9 @@ files = ( 61054EA22A6EE10B00AAA894 /* Scheduler.swift in Sources */, A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */, + A7B932FB2B1F6A0A00AE6477 /* EnrichedRecord.swift in Sources */, 61054E8D2A6EE10A00AAA894 /* RUMContextReceiver.swift in Sources */, + A7B932FD2B1F6A0A00AE6477 /* EnrichedResource.swift in Sources */, 61054E922A6EE10A00AAA894 /* SegmentJSONBuilder.swift in Sources */, 61054E622A6EE10A00AAA894 /* RecordWriter.swift in Sources */, 61054E692A6EE10A00AAA894 /* ImageDataProvider.swift in Sources */, @@ -7652,7 +7656,6 @@ 61054E822A6EE10A00AAA894 /* UILabelRecorder.swift in Sources */, A73A54982B16406900E1F7E3 /* ResourcesFeature.swift in Sources */, 61054E6C2A6EE10A00AAA894 /* SystemColors.swift in Sources */, - A71265902B179C94007D63CE /* SRDataModels+UIKit.swift in Sources */, 61054E812A6EE10A00AAA894 /* UIStepperRecorder.swift in Sources */, 61054E632A6EE10A00AAA894 /* SessionReplayConfiguration.swift in Sources */, 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */, @@ -7676,10 +7679,8 @@ 61054E9C2A6EE10B00AAA894 /* UIImage+Scaling.swift in Sources */, 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */, 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */, - A71265922B179C94007D63CE /* EnrichedRecord.swift in Sources */, 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, - A712658F2B179C94007D63CE /* EnrichedResource.swift in Sources */, 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */, 61054E6A2A6EE10A00AAA894 /* UIKitExtensions.swift in Sources */, 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */, @@ -7692,19 +7693,21 @@ 61054E642A6EE10A00AAA894 /* SessionReplay.swift in Sources */, 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */, 61054E722A6EE10A00AAA894 /* TouchIdentifierGenerator.swift in Sources */, + A7B932F52B1F694000AE6477 /* ResourcesProcessor.swift in Sources */, 61054E912A6EE10A00AAA894 /* EnrichedRecordJSON.swift in Sources */, 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */, 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */, 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */, 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */, + A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */, 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */, 61054E882A6EE10A00AAA894 /* ViewTreeRecordingContext.swift in Sources */, 61054E932A6EE10A00AAA894 /* MultipartFormData.swift in Sources */, 61054E712A6EE10A00AAA894 /* TouchSnapshot.swift in Sources */, 61054E8A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift in Sources */, 61054E7A2A6EE10A00AAA894 /* UIImageViewRecorder.swift in Sources */, - A71265912B179C94007D63CE /* SRDataModels.swift in Sources */, + A7B932FC2B1F6A0A00AE6477 /* SRDataModels.swift in Sources */, 61054E752A6EE10A00AAA894 /* ViewTreeSnapshot.swift in Sources */, 61054EA02A6EE10B00AAA894 /* Colors.swift in Sources */, 61054E7F2A6EE10A00AAA894 /* UISliderRecorder.swift in Sources */, diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index e6f232ee32..69e95cc470 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -10,6 +10,7 @@ import DatadogInternal internal class SessionReplayFeature: DatadogRemoteFeature { static let name: String = "session-replay" + let requestBuilder: FeatureRequestBuilder let messageReceiver: FeatureMessageReceiver let performanceOverride: PerformancePresetOverride? @@ -18,8 +19,6 @@ internal class SessionReplayFeature: DatadogRemoteFeature { /// Orchestrates the process of capturing next snapshots on the main thread. let recordingCoordinator: RecordingCoordinator - /// Processes each new snapshot on a background thread and transforms it into records. - let processor: SnapshotProcessing // MARK: - Initialization @@ -27,18 +26,24 @@ internal class SessionReplayFeature: DatadogRemoteFeature { core: DatadogCoreProtocol, configuration: SessionReplay.Configuration ) throws { - let processor = SnapshotProcessor( - queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.processor"), - writer: RecordWriter(core: core), + let snapshotProcessor = SnapshotProcessor( + queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.snapshot-processor"), + recordsWriter: RecordWriter(core: core), srContextPublisher: SRContextPublisher(core: core), telemetry: core.telemetry ) + let resourceProcessor = ResourceProcessor( + queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.resource-processor"), + writer: ResourcesWriter(core: core), + telemetry: core.telemetry + ) let scheduler = MainThreadScheduler(interval: 0.1) let messageReceiver = RUMContextReceiver() let recorder = try Recorder( - processor: processor, + snapshotProcessor: snapshotProcessor, + resourceProcessor: resourceProcessor, telemetry: core.telemetry, additionalNodeRecorders: configuration._additionalNodeRecorders ) @@ -53,7 +58,7 @@ internal class SessionReplayFeature: DatadogRemoteFeature { self.messageReceiver = messageReceiver self.recordingCoordinator = recordingCoordinator - self.processor = processor + self.requestBuilder = SegmentRequestBuilder( customUploadURL: configuration.customEndpoint, telemetry: core.telemetry diff --git a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift index 78e78fceb0..5ded938b62 100644 --- a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift +++ b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift @@ -8,7 +8,7 @@ import Foundation /// Extends the resource information with context. -internal struct EnrichedResource: Codable, Resource { +internal struct EnrichedResource: Hashable, Codable, Resource { internal struct Context: Codable, Equatable { internal struct Application: Codable, Equatable { let id: String @@ -38,5 +38,9 @@ internal struct EnrichedResource: Codable, Resource { self.data = data self.context = context } + + func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } } #endif diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift new file mode 100644 index 0000000000..f755ab8681 --- /dev/null +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.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. + */ + +#if os(iOS) +import Foundation +import DatadogInternal + +internal protocol ResourceProcessing { + func process(resources: [Resource], context: EnrichedResource.Context) +} + +internal class ResourceProcessor: ResourceProcessing { + /// The background queue for executing all logic. + private let queue: Queue + /// Writes records to `DatadogCore`. + private let writer: ResourcesWriting + /// Sends telemetry through sdk core. + private let telemetry: Telemetry + + func process(resources: [Resource], context: EnrichedResource.Context) { + queue.run { [writer] in + writer.write(resources: Set(resources.map { EnrichedResource(resource: $0, context: context) })) + } + } + + init(queue: Queue, writer: ResourcesWriting, telemetry: Telemetry) { + self.queue = queue + self.writer = writer + self.telemetry = telemetry + } +} +#endif diff --git a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift index 768e36a8ca..67d79c58d7 100644 --- a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift @@ -41,7 +41,7 @@ internal class SnapshotProcessor: SnapshotProcessing { /// The background queue for executing all logic. private let queue: Queue /// Writes records to `DatadogCore`. - private let writer: RecordWriting + private let recordsWriter: RecordWriting /// Sends telemetry through sdk core. private let telemetry: Telemetry @@ -62,12 +62,12 @@ internal class SnapshotProcessor: SnapshotProcessing { init( queue: Queue, - writer: RecordWriting, + recordsWriter: RecordWriting, srContextPublisher: SRContextPublisher, telemetry: Telemetry ) { self.queue = queue - self.writer = writer + self.recordsWriter = recordsWriter self.srContextPublisher = srContextPublisher self.telemetry = telemetry self.recordsBuilder = RecordsBuilder(telemetry: telemetry) @@ -132,7 +132,7 @@ internal class SnapshotProcessor: SnapshotProcessing { let enrichedRecord = EnrichedRecord(context: viewTreeSnapshot.context, records: records) trackRecord(key: enrichedRecord.viewID, value: Int64(records.count)) - writer.write(nextRecord: enrichedRecord) + recordsWriter.write(nextRecord: enrichedRecord) } // Track state: diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index 5fd0944227..30e8e5a60b 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -60,11 +60,14 @@ public class Recorder: Recording { private let touchSnapshotProducer: TouchSnapshotProducer /// Turns view tree snapshots into data models that will be uploaded to SR BE. private let snapshotProcessor: SnapshotProcessing + /// Processes resources on a background thread. + private let resourceProcessor: ResourceProcessing /// Sends telemetry through sdk core. private let telemetry: Telemetry convenience init( - processor: SnapshotProcessing, + snapshotProcessor: SnapshotProcessing, + resourceProcessor: ResourceProcessing, telemetry: Telemetry, additionalNodeRecorders: [NodeRecorder] ) throws { @@ -81,7 +84,8 @@ public class Recorder: Recording { uiApplicationSwizzler: try UIApplicationSwizzler(handler: touchSnapshotProducer), viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, - snapshotProcessor: processor, + snapshotProcessor: snapshotProcessor, + resourceProcessor: resourceProcessor, telemetry: telemetry ) } @@ -91,12 +95,14 @@ public class Recorder: Recording { viewTreeSnapshotProducer: ViewTreeSnapshotProducer, touchSnapshotProducer: TouchSnapshotProducer, snapshotProcessor: SnapshotProcessing, + resourceProcessor: ResourceProcessing, telemetry: Telemetry ) { self.uiApplicationSwizzler = uiApplicationSwizzler self.viewTreeSnapshotProducer = viewTreeSnapshotProducer self.touchSnapshotProducer = touchSnapshotProducer self.snapshotProcessor = snapshotProcessor + self.resourceProcessor = resourceProcessor self.telemetry = telemetry uiApplicationSwizzler.swizzle() } @@ -117,6 +123,13 @@ public class Recorder: Recording { } let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext) snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot) + + if !viewTreeSnapshot.resources.isEmpty { + resourceProcessor.process( + resources: viewTreeSnapshot.resources, + context: .init(recorderContext.applicationID) + ) + } } catch let error { telemetry.error("[SR] Failed to take snapshot", error: DDError(error: error)) } diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index 78cdd5ffa3..efbe4c8e49 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -74,10 +74,20 @@ fileprivate extension CGSize { } } +private var recordedKey: UInt8 = 22 extension UIImage { var srIdentifier: String { return customHash } + + var recorded: Bool { + get { + return objc_getAssociatedObject(self, &recordedKey) as? Bool ?? false + } + set { + objc_setAssociatedObject(self, &recordedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } } extension UIColor { @@ -90,14 +100,18 @@ import CryptoKit private var customHashKey: UInt8 = 11 fileprivate extension UIImage { + private static var associatedObjectQueue = DispatchQueue(label: "com.datadoghq.customHashQueue") + var customHash: String { - if let hash = objc_getAssociatedObject(self, &customHashKey) as? String { + return UIImage.associatedObjectQueue.sync { + if let hash = objc_getAssociatedObject(self, &customHashKey) as? String { + return hash + } + + let hash = computeHash() + objc_setAssociatedObject(self, &customHashKey, hash, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return hash } - - let hash = computeHash() - objc_setAssociatedObject(self, &customHashKey, hash, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return hash } private func computeHash() -> String { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift index 2783cb9dfe..cfe47ea5bb 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift @@ -22,26 +22,25 @@ internal struct UIDatePickerRecorder: NodeRecorder { return InvisibleElement.constant } - var nodes: [Node] = [] - + var recordingResult: RecordingResult? if #available(iOS 13.4, *) { switch datePicker.datePickerStyle { case .wheels: - nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + recordingResult = wheelsStyleRecorder.record(datePicker, with: attributes, in: context) case .compact: - nodes = compactStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + recordingResult = compactStyleRecorder.record(datePicker, with: attributes, in: context) case .inline: - nodes = inlineStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + recordingResult = inlineStyleRecorder.record(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) + recordingResult = wheelsStyleRecorder.record(datePicker, with: attributes, in: context) } } else { // Observation: older OS versions use the "wheels" style - nodes = wheelsStyleRecorder.recordNodes(of: datePicker, with: attributes, in: context) + recordingResult = wheelsStyleRecorder.record(datePicker, with: attributes, in: context) } let isDisplayedInPopover: Bool = { @@ -65,7 +64,8 @@ internal struct UIDatePickerRecorder: NodeRecorder { ) return SpecificElement( subtreeStrategy: .ignore, - nodes: [backgroundNode] + nodes + nodes: [backgroundNode] + (recordingResult?.nodes ?? []), + resources: recordingResult?.resources ?? [] ) } } @@ -81,8 +81,8 @@ private struct WheelsStyleDatePickerRecorder { ] ) - func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { - return pickerTreeRecorder.recordNodes(for: view, in: context) + func record(_ view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> RecordingResult { + return pickerTreeRecorder.record(view, in: context) } } @@ -108,7 +108,7 @@ private struct InlineStyleDatePickerRecorder { ) } - func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { + func record(_ view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> RecordingResult { viewRecorder.semanticsOverride = { _, viewAttributes in if context.recorder.privacy.shouldMaskInputElements { let isSquare = viewAttributes.frame.width == viewAttributes.frame.height @@ -128,7 +128,7 @@ private struct InlineStyleDatePickerRecorder { } } - return subtreeRecorder.recordNodes(for: view, in: context) + return subtreeRecorder.record(view, in: context) } } @@ -144,8 +144,8 @@ private struct CompactStyleDatePickerRecorder { ] ) - func recordNodes(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> [Node] { - return subtreeRecorder.recordNodes(for: view, in: context) + func record(_ view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> RecordingResult { + return subtreeRecorder.record(view, in: context) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 3c5443bc47..3679c70343 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -6,6 +6,16 @@ #if os(iOS) import UIKit +@_spi(Internal) + +extension UIImage: SessionReplayResource { + public var identifier: String { + return srIdentifier + } + public var data: Data { + return scaledDownToApproximateSize(256.KB) + } +} internal struct UIImageViewRecorder: NodeRecorder { internal let identifier = UUID() @@ -62,6 +72,7 @@ internal struct UIImageViewRecorder: NodeRecorder { } else { contentFrame = nil } + let shouldRecordImage = shouldRecordImagePredicate(imageView) let builder = UIImageViewWireframesBuilder( wireframeID: ids[0], imageWireframeID: ids[1], @@ -71,10 +82,19 @@ internal struct UIImageViewRecorder: NodeRecorder { image: imageView.image, imageDataProvider: context.imageDataProvider, tintColor: tintColorProvider(imageView), - shouldRecordImage: shouldRecordImagePredicate(imageView) + shouldRecordImage: shouldRecordImage ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - return SpecificElement(subtreeStrategy: .record, nodes: [node]) + return SpecificElement( + subtreeStrategy: .record, + nodes: [node], + resources: [imageView.image].filter { image in + defer { + image?.recorded = true + } + return image?.recorded == false && shouldRecordImage + }.compactMap { $0 } + ) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift index 8dd23325b3..5587e306eb 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift @@ -71,13 +71,17 @@ internal struct UIPickerViewRecorder: NodeRecorder { // in the actual `UIPickerView's` tree their order is opposite (blending is used to make the label // pass through the shape). For that reason, we record both kinds of nodes separately and then reorder // them in returned semantics: - let backgroundNodes = selectionRecorder.recordNodes(for: picker, in: context) - let titleNodes = labelsRecorder.recordNodes(for: picker, in: context) + let backgroundRecordingResult = selectionRecorder.record(picker, in: context) + let titleRecordingResult = labelsRecorder.record(picker, in: context) guard attributes.hasAnyAppearance else { // If the root view of `UIPickerView` defines no other appearance (e.g. no custom `.background`), we can // safely ignore it, with only forwarding child nodes to final recording. - return SpecificElement(subtreeStrategy: .ignore, nodes: backgroundNodes + titleNodes) + return SpecificElement( + subtreeStrategy: .ignore, + nodes: backgroundRecordingResult.nodes + titleRecordingResult.nodes, + resources: backgroundRecordingResult.resources + titleRecordingResult.resources + ) } // Otherwise, we build dedicated wireframes to describe extra appearance coming from picker's root `UIView`: @@ -87,7 +91,11 @@ internal struct UIPickerViewRecorder: NodeRecorder { backgroundWireframeID: context.ids.nodeID(view: picker, nodeRecorder: self) ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - return SpecificElement(subtreeStrategy: .ignore, nodes: [node] + backgroundNodes + titleNodes) + return SpecificElement( + subtreeStrategy: .ignore, + nodes: [node] + backgroundRecordingResult.nodes + titleRecordingResult.nodes, + resources: backgroundRecordingResult.resources + titleRecordingResult.resources + ) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift index e1b835d566..5a0f22ed2d 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift @@ -42,7 +42,8 @@ internal struct UITextFieldRecorder: NodeRecorder { // For our "approximation", we render text field's text on top of other TF's appearance. // Here we record both kind of nodes separately and order them respectively in returned semantics: - let appearanceNodes = recordAppearance(in: textField, textFieldAttributes: attributes, using: context) + let appearanceRecordingResult = recordAppearance(in: textField, textFieldAttributes: attributes, using: context) + let appearanceNodes = appearanceRecordingResult.nodes if let textNode = recordText(in: textField, attributes: attributes, using: context) { return SpecificElement(subtreeStrategy: .ignore, nodes: appearanceNodes + [textNode]) } else { @@ -51,7 +52,7 @@ internal struct UITextFieldRecorder: NodeRecorder { } /// Records `UIView` and `UIImageViewRecorder` nodes that define text field's appearance. - private func recordAppearance(in textField: UITextField, textFieldAttributes: ViewAttributes, using context: ViewTreeRecordingContext) -> [Node] { + private func recordAppearance(in textField: UITextField, textFieldAttributes: ViewAttributes, using context: ViewTreeRecordingContext) -> RecordingResult { backgroundViewRecorder.semanticsOverride = { _, viewAttributes in // We consider view to define text field's appearance if it has the same // size as text field: @@ -60,7 +61,7 @@ internal struct UITextFieldRecorder: NodeRecorder { return !isBackground ? IgnoredElement(subtreeStrategy: .record) : nil } - return subtreeRecorder.recordNodes(for: textField, in: context) + return subtreeRecorder.record(textField, in: context) } /// Creates node that represents TF's text. diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift index 50805acbeb..de4b3c9caf 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift @@ -50,7 +50,7 @@ internal class UIViewRecorder: NodeRecorder { attributes: attributes ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) - return AmbiguousElement(nodes: [node]) + return AmbiguousElement(nodes: [node], resources: []) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift index db31d70cae..d166751031 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift @@ -7,6 +7,11 @@ #if os(iOS) import UIKit +internal struct RecordingResult { + let nodes: [Node] + let resources: [Resource] +} + internal struct ViewTreeRecorder { /// An array of enabled node recorders. /// @@ -15,15 +20,21 @@ internal struct ViewTreeRecorder { let nodeRecorders: [NodeRecorder] /// Creates `Nodes` for given view and its subtree hierarchy. - func recordNodes(for anyView: UIView, in context: ViewTreeRecordingContext) -> [Node] { + func record(_ anyView: UIView, in context: ViewTreeRecordingContext) -> RecordingResult { var nodes: [Node] = [] - recordRecursively(nodes: &nodes, view: anyView, context: context) - return nodes + var resources: [Resource] = [] + recordRecursively(nodes: &nodes, resources: &resources, view: anyView, context: context) + return RecordingResult(nodes: nodes, resources: resources) } // MARK: - Private - private func recordRecursively(nodes: inout [Node], view: UIView, context: ViewTreeRecordingContext) { + private func recordRecursively( + nodes: inout [Node], + resources: inout [Resource], + view: UIView, + context: ViewTreeRecordingContext + ) { var context = context if let viewController = view.next as? UIViewController { context.viewControllerContext.parentType = .init(viewController) @@ -37,11 +48,14 @@ internal struct ViewTreeRecorder { if !semantics.nodes.isEmpty { nodes.append(contentsOf: semantics.nodes) } + if !semantics.resources.isEmpty { + resources.append(contentsOf: semantics.resources) + } switch semantics.subtreeStrategy { case .record: for subview in view.subviews { - recordRecursively(nodes: &nodes, view: subview, context: context) + recordRecursively(nodes: &nodes, resources: &resources, view: subview, context: context) } case .ignore: break diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 8472fee09f..19a7508d96 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -25,6 +25,9 @@ internal struct ViewTreeSnapshot { let viewportSize: CGSize /// An array of nodes recorded for this snapshot - sequenced in DFS order. let nodes: [Node] + /// An array of resource references recorded for this snapshot - sequenced in DFS order. + /// May contain references to the same resource if it appears multiple times in the snapshot. + let resources: [Resource] } /// An individual node in `ViewTreeSnapshot`. A `SessionReplayNode` describes a single view - similar: an array of nodes describes @@ -169,6 +172,8 @@ public protocol SessionReplayNodeSemantics { var subtreeStrategy: SessionReplayNodeSubtreeStrategy { get } /// Nodes that share this semantics. var nodes: [SessionReplayNode] { get } + /// Resources collected while traversing the subtree of this node. + var resources: [SessionReplayResource] { get } } // This alias enables us to have a more unique name exposed through public-internal access level @@ -205,6 +210,7 @@ internal struct UnknownElement: NodeSemantics { static let importance: Int = .min let subtreeStrategy: NodeSubtreeStrategy = .record let nodes: [Node] = [] + let resources: [Resource] = [] /// Use `UnknownElement.constant` instead. private init () {} @@ -222,6 +228,7 @@ public struct SessionReplayInvisibleElement: SessionReplayNodeSemantics { public static let importance: Int = 0 public let subtreeStrategy: SessionReplayNodeSubtreeStrategy public let nodes: [SessionReplayNode] = [] + public let resources: [SessionReplayResource] = [] /// Use `InvisibleElement.constant` instead. private init () { @@ -245,6 +252,7 @@ internal struct IgnoredElement: NodeSemantics { static var importance: Int = .max let subtreeStrategy: NodeSubtreeStrategy let nodes: [Node] = [] + let resources: [Resource] = [] } /// A semantics of an UI element that is of `UIView` type. This semantics mean that the element has visual appearance in SR, but @@ -255,6 +263,7 @@ internal struct AmbiguousElement: NodeSemantics { static let importance: Int = 0 let subtreeStrategy: NodeSubtreeStrategy = .record let nodes: [Node] + let resources: [Resource] } /// A semantics of an UI element that is one of `UIView` subclasses. This semantics mean that we know its full identity along with set of @@ -265,10 +274,16 @@ public struct SessionReplaySpecificElement: SessionReplayNodeSemantics { public static let importance: Int = .max public let subtreeStrategy: SessionReplayNodeSubtreeStrategy public let nodes: [SessionReplayNode] + public let resources: [SessionReplayResource] - public init(subtreeStrategy: SessionReplayNodeSubtreeStrategy, nodes: [SessionReplayNode]) { + public init( + subtreeStrategy: SessionReplayNodeSubtreeStrategy, + nodes: [SessionReplayNode], + resources: [SessionReplayResource] = [] + ) { self.subtreeStrategy = subtreeStrategy self.nodes = nodes + self.resources = resources } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 66590ae468..d32f29909d 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -33,11 +33,13 @@ internal struct ViewTreeSnapshotBuilder { ids: idsGenerator, imageDataProvider: imageDataProvider ) + let recording = viewTreeRecorder.record(rootView, in: context) let snapshot = ViewTreeSnapshot( date: recorderContext.date.addingTimeInterval(recorderContext.viewServerTimeOffset ?? 0), context: recorderContext, viewportSize: rootView.bounds.size, - nodes: viewTreeRecorder.recordNodes(for: rootView, in: context) + nodes: recording.nodes, + resources: recording.resources ) return snapshot } diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index cabfb4bbd8..a1bff8c844 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -9,12 +9,12 @@ import Foundation import DatadogInternal /// A type writing Session Replay records to `DatadogCore`. -internal protocol ResourceWriting { +internal protocol ResourcesWriting { /// Writes next records to SDK core. - func write(resources: [EnrichedResource]) + func write(resources: Set) } -internal class ResourcesWriter: ResourceWriting { +internal class ResourcesWriter: ResourcesWriting { /// An instance of SDK core the SR feature is registered to. private weak var core: DatadogCoreProtocol? @@ -26,7 +26,7 @@ internal class ResourcesWriter: ResourceWriting { // MARK: - Writing - func write(resources: [EnrichedResource]) { + func write(resources: Set) { guard let scope = core?.scope(for: ResourcesFeature.name) else { return } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index 0e07b22750..6c022d715a 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -33,7 +33,8 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { date: .mockRandom(), context: .mockRandom(), viewportSize: .mockRandom(), - nodes: .mockRandom(count: .random(in: (5..<50))) + nodes: .mockRandom(count: .random(in: (5..<50))), + resources: .mockRandom(count: .random(in: (5..<50))) ) } @@ -41,13 +42,15 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { date: Date = .mockAny(), context: Recorder.Context = .mockAny(), viewportSize: CGSize = .mockAny(), - nodes: [Node] = .mockAny() + nodes: [Node] = .mockAny(), + resources: [Resource] = .mockAny() ) -> ViewTreeSnapshot { return ViewTreeSnapshot( date: date, context: context, viewportSize: viewportSize, - nodes: nodes + nodes: nodes, + resources: resources ) } } @@ -221,7 +224,10 @@ func mockRandomNodeSemantics() -> NodeSemantics { let all: [NodeSemantics] = [ UnknownElement.constant, InvisibleElement.constant, - AmbiguousElement(nodes: .mockRandom(count: .mockRandom(min: 1, max: 5))), + AmbiguousElement( + nodes: .mockRandom(count: .mockRandom(min: 1, max: 5)), + resources: .mockRandom(count: .mockRandom(min: 1, max: 5)) + ), SpecificElement(subtreeStrategy: .mockRandom(), nodes: .mockRandom(count: .mockRandom(min: 1, max: 5))), ] return all.randomElement()! @@ -259,6 +265,34 @@ extension Node: AnyMockable, RandomMockable { } } +struct MockResource: Resource, AnyMockable, RandomMockable { + var identifier: String + let data: Data + + init(identifier: String, data: Data) { + self.identifier = identifier + self.data = data + } + + static func mockAny() -> MockResource { + return MockResource(identifier: .mockAny(), data: .mockAny()) + } + + static func mockRandom() -> MockResource { + return MockResource(identifier: . mockRandom(), data: .mockRandom()) + } +} + +extension Collection where Element == Resource { + static func mockAny() -> [Resource] { + return [MockResource].mockAny() + } + + static func mockRandom(count: Int = 10) -> [Resource] { + return [MockResource].mockRandom(count: count) + } +} + extension SpecificElement { static func mockAny() -> SpecificElement { SpecificElement(subtreeStrategy: .mockRandom(), nodes: []) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index c29f44612d..df4d268b33 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -15,6 +15,7 @@ private struct MockSemantics: NodeSemantics { static var importance: Int = .mockAny() var subtreeStrategy: NodeSubtreeStrategy var nodes: [Node] + var resources: [Resource] init(subtreeStrategy: NodeSubtreeStrategy, nodeNames: [String]) { self.subtreeStrategy = subtreeStrategy @@ -48,7 +49,7 @@ class ViewTreeRecorderTests: XCTestCase { NodeRecorderMock(resultForView: { _ in nil }), ] let recorder = ViewTreeRecorder(nodeRecorders: recorders) - _ = recorder.recordNodes(for: rootView, in: .mockAny()) + _ = recorder.record(rootView, in: .mockAny()) // Then XCTAssertEqual(recorders[0].queriedViews, [rootView, childView, grandchildView]) @@ -59,9 +60,9 @@ class ViewTreeRecorderTests: XCTestCase { func testItQueriesNodeRecordersUntilOneFindsBestSemantics() { // Given let view = UIView(frame: .mockRandom()) - + Node.mockAny() let unknownElement = UnknownElement.constant - let ambiguousElement = AmbiguousElement(nodes: .mockAny()) + let ambiguousElement = AmbiguousElement(nodes: .mockAny(), resources: .mockAny()) let specificElement = SpecificElement(subtreeStrategy: .mockRandom(), nodes: .mockAny()) // When @@ -73,7 +74,7 @@ class ViewTreeRecorderTests: XCTestCase { NodeRecorderMock(resultForView: { _ in specificElement }), // ... so this one should not be queried ] let recorder = ViewTreeRecorder(nodeRecorders: recorders) - _ = recorder.recordNodes(for: view, in: .mockAny()) + _ = recorder.record(view, in: .mockAny()) // Then XCTAssertEqual(recorders[0].queriedViews, [view], "It should be queried as semantics is not known") @@ -131,7 +132,7 @@ class ViewTreeRecorderTests: XCTestCase { // When let nodeRecorder = NodeRecorderMock(resultForView: { view in semanticsByView[view] }) let recorder = ViewTreeRecorder(nodeRecorders: [nodeRecorder]) - let nodes = recorder.recordNodes(for: rootView, in: .mockRandom()) + let nodes = recorder.record(rootView, in: .mockRandom()) // Then let expectedNodes = ["rootView", "a", "aa", "aav1", "aav2", "aav3", "ab", "aba", "abb", "b", "c"] @@ -161,7 +162,7 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.recordNodes(for: view, in: .mockRandom()) + let nodes = recorder.record(view, in: .mockRandom()) // Then XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for \(type(of: view)) when it is not visible") @@ -179,19 +180,19 @@ class ViewTreeRecorderTests: XCTestCase { let `switch` = UISwitch.mock(withFixture: .visible(.noAppearance)) // When - let viewNodes = recorder.recordNodes(for: view, in: .mockRandom()) + let viewNodes = recorder.record(view, in: .mockRandom()) XCTAssertTrue(viewNodes.isEmpty, "No nodes should be recorded for `UIView` when it has no appearance") - let labelNodes = recorder.recordNodes(for: label, in: .mockRandom()) + let labelNodes = recorder.record(label, in: .mockRandom()) XCTAssertTrue(labelNodes.isEmpty, "No nodes should be recorded for `UILabel` when it has no appearance") - let imageViewNodes = recorder.recordNodes(for: imageView, in: .mockRandom()) + let imageViewNodes = recorder.record(imageView, in: .mockRandom()) XCTAssertTrue(imageViewNodes.isEmpty, "No nodes should be recorded for `UIImageView` when it has no appearance") - let textFieldNodes = recorder.recordNodes(for: textField, in: .mockRandom()) + let textFieldNodes = recorder.record(textField, in: .mockRandom()) XCTAssertTrue(textFieldNodes.isEmpty, "No nodes should be recorded for `UITextField` when it has no appearance") - let switchNodes = recorder.recordNodes(for: `switch`, in: .mockRandom()) + let switchNodes = recorder.record(`switch`, in: .mockRandom()) XCTAssertFalse( switchNodes.isEmpty, "`UISwitch` with no appearance should record some nodes as it has style coming from its internal subtree." @@ -213,7 +214,7 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.recordNodes(for: view, in: .mockRandom()) + let nodes = recorder.record(view, in: .mockRandom()) // Then XCTAssertFalse(nodes.isEmpty, "Some nodes should be recorded for \(type(of: view)) when it has some appearance") @@ -239,7 +240,7 @@ class ViewTreeRecorderTests: XCTestCase { } // When - _ = recorder.recordNodes(for: views.first!, in: .mockRandom()) + _ = recorder.record(views.first!, in: .mockRandom()) // Then var context = nodeRecorder.queryContextsByView[views[0]] From 3434eda7ea4635b411ec20fda1609ac5599028f6 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 12 Dec 2023 16:37:34 +0000 Subject: [PATCH 04/60] RUM-2153 Add tests --- Datadog/Datadog.xcodeproj/project.pbxproj | 24 ++++++--- .../Tests/Datadog/RUM/RUMDebuggingTests.swift | 10 ++-- .../Feature/SessionReplayFeature.swift | 4 +- .../Processor/ResourcesProcessor.swift | 10 ++-- .../Sources/Processor/SnapshotProcessor.swift | 8 +-- .../Sources/Writers/RecordWriter.swift | 4 +- .../Sources/Writers/ResourcesWriter.swift | 4 +- .../Tests/Mocks/ResourceMocks.swift | 18 ++++++- .../Tests/Mocks/ResourceProcessorSpy.swift | 18 +++++++ ...orSpy.swift => SnapshotProcessorSpy.swift} | 2 +- .../Processor/ResourceProcessorTests.swift | 46 ++++++++++++++++ ...sts.swift => SnapshotProcessorTests.swift} | 52 +++++++++++-------- .../Tests/Recorder/RecorderTests.swift | 19 ++++--- .../ViewTreeRecorderTests.swift | 42 ++++++++------- .../ViewTreeSnapshotTests.swift | 2 +- 15 files changed, 183 insertions(+), 80 deletions(-) create mode 100644 DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift rename DatadogSessionReplay/Tests/Mocks/{ProcessorSpy.swift => SnapshotProcessorSpy.swift} (92%) create mode 100644 DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift rename DatadogSessionReplay/Tests/Processor/{ProcessorTests.swift => SnapshotProcessorTests.swift} (89%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index e095250c24..a1dd0bd9c4 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 61054FA32A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F522A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift */; }; 61054FA42A6EE1BA00AAA894 /* DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F532A6EE1BA00AAA894 /* DiffTests.swift */; }; 61054FA52A6EE1BA00AAA894 /* RecordsBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */; }; - 61054FA62A6EE1BA00AAA894 /* ProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F562A6EE1BA00AAA894 /* ProcessorTests.swift */; }; + 61054FA62A6EE1BA00AAA894 /* SnapshotProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */; }; 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */; }; 61054FA82A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */; }; 61054FAA2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5D2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift */; }; @@ -189,7 +189,7 @@ 61054FC52A6EE1BA00AAA894 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */; }; 61054FC62A6EE1BA00AAA894 /* CoreGraphicsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */; }; 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */; }; - 61054FC82A6EE1BA00AAA894 /* ProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F812A6EE1BA00AAA894 /* ProcessorSpy.swift */; }; + 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */; }; 61054FC92A6EE1BA00AAA894 /* RecorderMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */; }; 61054FCA2A6EE1BA00AAA894 /* TestScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */; }; 61054FCB2A6EE1BA00AAA894 /* QueueMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */; }; @@ -521,6 +521,8 @@ A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */; }; A7C816AB2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; A7C816AC2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; + A7D9528A2B28BD94004C79B1 /* ResourceProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */; }; + A7D9528C2B28C18D004C79B1 /* ResourceProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */; }; A7DA18042AB0C91200F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; A7DA18052AB0C91300F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; A7DA18072AB0CA5E00F76337 /* DDUIKitRUMActionsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */; }; @@ -1987,7 +1989,7 @@ 61054F522A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Diff+SRWireframesTests.swift"; sourceTree = ""; }; 61054F532A6EE1BA00AAA894 /* DiffTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = ""; }; 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordsBuilderTests.swift; sourceTree = ""; }; - 61054F562A6EE1BA00AAA894 /* ProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessorTests.swift; sourceTree = ""; }; + 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessorTests.swift; sourceTree = ""; }; 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesFlattenerTests.swift; sourceTree = ""; }; 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinatorTests.swift; sourceTree = ""; }; 61054F5D2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensionsTests.swift; sourceTree = ""; }; @@ -2020,7 +2022,7 @@ 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphicsMocks.swift; sourceTree = ""; }; 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRDataModelsMocks.swift; sourceTree = ""; }; - 61054F812A6EE1BA00AAA894 /* ProcessorSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessorSpy.swift; sourceTree = ""; }; + 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessorSpy.swift; sourceTree = ""; }; 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderMocks.swift; sourceTree = ""; }; 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestScheduler.swift; sourceTree = ""; }; 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueMocks.swift; sourceTree = ""; }; @@ -2447,6 +2449,8 @@ A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedResource.swift; sourceTree = ""; }; A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SRDataModels+UIKit.swift"; sourceTree = ""; }; A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; + A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorSpy.swift; sourceTree = ""; }; + A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorTests.swift; sourceTree = ""; }; A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMActionsPredicateTests.swift; sourceTree = ""; }; A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriter.swift; sourceTree = ""; }; @@ -3421,7 +3425,8 @@ 61054F4F2A6EE1BA00AAA894 /* Privacy */, 61054F512A6EE1BA00AAA894 /* Diffing */, 61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */, - 61054F562A6EE1BA00AAA894 /* ProcessorTests.swift */, + 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */, + A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */, 61054F572A6EE1BA00AAA894 /* Flattening */, ); path = Processor; @@ -3549,7 +3554,7 @@ 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */, 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */, 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */, - 61054F812A6EE1BA00AAA894 /* ProcessorSpy.swift */, + 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */, 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */, 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */, 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */, @@ -3559,6 +3564,7 @@ A74A72862B10CE4100771FEB /* ResourceMocks.swift */, A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, A71265852B17980C007D63CE /* MockFeature.swift */, + A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */, ); path = Mocks; sourceTree = ""; @@ -7758,7 +7764,7 @@ 61054F952A6EE1BA00AAA894 /* SessionReplayConfigurationTests.swift in Sources */, 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */, 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */, - 61054FC82A6EE1BA00AAA894 /* ProcessorSpy.swift in Sources */, + 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */, A74A72872B10CE4100771FEB /* ResourceMocks.swift in Sources */, 61054FA42A6EE1BA00AAA894 /* DiffTests.swift in Sources */, 61054FA02A6EE1BA00AAA894 /* SRCompressionTests.swift in Sources */, @@ -7772,8 +7778,10 @@ 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */, 61054FC52A6EE1BA00AAA894 /* UIKitMocks.swift in Sources */, 61054FB92A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift in Sources */, - 61054FA62A6EE1BA00AAA894 /* ProcessorTests.swift in Sources */, + 61054FA62A6EE1BA00AAA894 /* SnapshotProcessorTests.swift in Sources */, 61054FB72A6EE1BA00AAA894 /* UISegmentRecorderTests.swift in Sources */, + A7D9528A2B28BD94004C79B1 /* ResourceProcessorSpy.swift in Sources */, + A7D9528C2B28C18D004C79B1 /* ResourceProcessorTests.swift in Sources */, A74A72892B10D95D00771FEB /* MultipartBuilderSpy.swift in Sources */, 61054FCF2A6EE1BA00AAA894 /* RUMContextReceiverTests.swift in Sources */, 61054FC92A6EE1BA00AAA894 /* RecorderMocks.swift in Sources */, diff --git a/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift index b3ca57449d..84daabfcdf 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift @@ -24,7 +24,7 @@ class RUMDebuggingTests: XCTestCase { _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "FirstView"), context: .mockAny(), - writer: FileWriterMock() + recordsWriter: FileWriterMock() ) let debugging = RUMDebugging() @@ -47,7 +47,7 @@ class RUMDebuggingTests: XCTestCase { func testWhenOneRUMViewIsInactive_andSecondIsActive_itDisplaysTwoRUMViewOutlines() throws { let context: DatadogContext = .mockAny() - let writer = FileWriterMock() + let recordsWriter = FileWriterMock() let expectation = self.expectation(description: "Render RUMDebugging") @@ -58,17 +58,17 @@ class RUMDebuggingTests: XCTestCase { _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "FirstView"), context: context, - writer: writer + recordsWriter: recordsWriter ) _ = applicationScope.process( command: RUMStartResourceCommand.mockAny(), context: context, - writer: writer + recordsWriter: recordsWriter ) _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "SecondView"), context: context, - writer: writer + recordsWriter: recordsWriter ) let debugging = RUMDebugging() diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 69e95cc470..6d35fb1624 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -28,13 +28,13 @@ internal class SessionReplayFeature: DatadogRemoteFeature { ) throws { let snapshotProcessor = SnapshotProcessor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.snapshot-processor"), - recordsWriter: RecordWriter(core: core), + recordWriter: RecordWriter(core: core), srContextPublisher: SRContextPublisher(core: core), telemetry: core.telemetry ) let resourceProcessor = ResourceProcessor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.resource-processor"), - writer: ResourcesWriter(core: core), + resourcesWriter: ResourcesWriter(core: core), telemetry: core.telemetry ) diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift index f755ab8681..bc3fa61c44 100644 --- a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift @@ -16,19 +16,19 @@ internal class ResourceProcessor: ResourceProcessing { /// The background queue for executing all logic. private let queue: Queue /// Writes records to `DatadogCore`. - private let writer: ResourcesWriting + private let resourcesWriter: ResourcesWriting /// Sends telemetry through sdk core. private let telemetry: Telemetry func process(resources: [Resource], context: EnrichedResource.Context) { - queue.run { [writer] in - writer.write(resources: Set(resources.map { EnrichedResource(resource: $0, context: context) })) + queue.run { [resourcesWriter] in + resourcesWriter.write(resources: Set(resources.map { EnrichedResource(resource: $0, context: context) })) } } - init(queue: Queue, writer: ResourcesWriting, telemetry: Telemetry) { + init(queue: Queue, resourcesWriter: ResourcesWriting, telemetry: Telemetry) { self.queue = queue - self.writer = writer + self.resourcesWriter = resourcesWriter self.telemetry = telemetry } } diff --git a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift index 67d79c58d7..f5eab564b0 100644 --- a/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift @@ -41,7 +41,7 @@ internal class SnapshotProcessor: SnapshotProcessing { /// The background queue for executing all logic. private let queue: Queue /// Writes records to `DatadogCore`. - private let recordsWriter: RecordWriting + private let recordWriter: RecordWriting /// Sends telemetry through sdk core. private let telemetry: Telemetry @@ -62,12 +62,12 @@ internal class SnapshotProcessor: SnapshotProcessing { init( queue: Queue, - recordsWriter: RecordWriting, + recordWriter: RecordWriting, srContextPublisher: SRContextPublisher, telemetry: Telemetry ) { self.queue = queue - self.recordsWriter = recordsWriter + self.recordWriter = recordWriter self.srContextPublisher = srContextPublisher self.telemetry = telemetry self.recordsBuilder = RecordsBuilder(telemetry: telemetry) @@ -132,7 +132,7 @@ internal class SnapshotProcessor: SnapshotProcessing { let enrichedRecord = EnrichedRecord(context: viewTreeSnapshot.context, records: records) trackRecord(key: enrichedRecord.viewID, value: Int64(records.count)) - recordsWriter.write(nextRecord: enrichedRecord) + recordWriter.write(nextRecord: enrichedRecord) } // Track state: diff --git a/DatadogSessionReplay/Sources/Writers/RecordWriter.swift b/DatadogSessionReplay/Sources/Writers/RecordWriter.swift index e4d7647979..9f6d7af060 100644 --- a/DatadogSessionReplay/Sources/Writers/RecordWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/RecordWriter.swift @@ -42,8 +42,8 @@ internal class RecordWriter: RecordWriting { return } - scope.eventWriteContext(bypassConsent: false, forceNewBatch: forceNewBatch) { _, writer in - writer.write(value: nextRecord) + scope.eventWriteContext(bypassConsent: false, forceNewBatch: forceNewBatch) { _, recordsWriter in + recordsWriter.write(value: nextRecord) } } } diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index a1bff8c844..4a2be5719c 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -30,8 +30,8 @@ internal class ResourcesWriter: ResourcesWriting { guard let scope = core?.scope(for: ResourcesFeature.name) else { return } - scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, writer in - writer.write(value: resources) + scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, recordsWriter in + recordsWriter.write(value: resources) } } } diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift index c9aadad3af..8b7c937d79 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift @@ -8,7 +8,15 @@ import Foundation import TestUtilities @testable import DatadogSessionReplay -extension EnrichedResource: RandomMockable { +extension EnrichedResource: RandomMockable, AnyMockable { + public static func mockAny() -> EnrichedResource { + return .init( + identifier: .mockAny(), + data: .mockAny(), + context: .mockAny() + ) + } + public static func mockRandom() -> Self { return .init( identifier: .mockRandom(), @@ -18,7 +26,13 @@ extension EnrichedResource: RandomMockable { } } -extension EnrichedResource.Context: RandomMockable { +extension EnrichedResource.Context: RandomMockable, AnyMockable { + public static func mockAny() -> DatadogSessionReplay.EnrichedResource.Context { + return .init( + .mockAny() + ) + } + public static func mockRandom() -> Self { return .init( .mockRandom() diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift new file mode 100644 index 0000000000..ff46c917dc --- /dev/null +++ b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift @@ -0,0 +1,18 @@ +/* + * 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 +@testable import DatadogSessionReplay + +/// Spies the interaction with `Processing`. +internal class ResourceProcessorSpy: ResourceProcessing { + var processedResources: [([Resource], EnrichedResource.Context)] = [] + + func process(resources: [Resource], context: EnrichedResource.Context) { + processedResources.append((resources, context)) + } + +} diff --git a/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift similarity index 92% rename from DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift rename to DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift index 9f52d9daa0..4d6f20e10b 100644 --- a/DatadogSessionReplay/Tests/Mocks/ProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/SnapshotProcessorSpy.swift @@ -8,7 +8,7 @@ import Foundation @testable import DatadogSessionReplay /// Spies the interaction with `Processing`. -internal class ProcessorSpy: SnapshotProcessing { +internal class SnapshotProcessorSpy: SnapshotProcessing { /// An array of snapshots recorded in `process(viewTreeSnapshot:touchSnapshot:)` private(set) var processedSnapshots: [(viewTreeSnapshot: ViewTreeSnapshot, touchSnapshot: TouchSnapshot?)] = [] diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift new file mode 100644 index 0000000000..6e2df458ee --- /dev/null +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -0,0 +1,46 @@ +/* + * 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 +import DatadogInternal +import TestUtilities + +@_spi(Internal) +@testable import DatadogSessionReplay + +private class ResourceWriterMock: ResourcesWriting { + var resources: [Set] = [] + + func write(resources: Set) { + self.resources.append(resources) + } +} + +class ResourceProcessorTests: XCTestCase { + func testItWritesResources() { + let writer = ResourceWriterMock() + let processor = ResourceProcessor( + queue: NoQueue(), + resourcesWriter: writer, + telemetry: NOPTelemetry() + ) + + let resource1: MockResource = .mockAny() + let resource2: MockResource = .mockAny() + let context: EnrichedResource.Context = .mockAny() + + processor.process(resources: [resource1, resource2], context: context) + + XCTAssertEqual(writer.resources.count, 1) + XCTAssertEqual( + writer.resources[0], + Set([ + EnrichedResource(resource: resource1, context: context), + EnrichedResource(resource: resource2, context: context) + ]) + ) + } +} diff --git a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift similarity index 89% rename from DatadogSessionReplay/Tests/Processor/ProcessorTests.swift rename to DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift index ddb462c206..172c3ec9a7 100644 --- a/DatadogSessionReplay/Tests/Processor/ProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift @@ -11,14 +11,14 @@ import TestUtilities @_spi(Internal) @testable import DatadogSessionReplay -private class WriterMock: RecordWriting { +private class RecordWriterMock: RecordWriting { var records: [EnrichedRecord] = [] func write(nextRecord: EnrichedRecord) { records.append(nextRecord) } } -class ProcessorTests: XCTestCase { - private let writer = WriterMock() +class SnapshotProcessorTests: XCTestCase { + private let recordWriter = RecordWriterMock() // MARK: - Processing `ViewTreeSnapshots` @@ -29,7 +29,12 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor( + queue: NoQueue(), + recordWriter: recordWriter, + srContextPublisher: srContextPublisher, + telemetry: TelemetryMock() + ) let viewTree = generateSimpleViewTree() // When @@ -37,9 +42,9 @@ class ProcessorTests: XCTestCase { processor.process(viewTreeSnapshot: snapshot, touchSnapshot: nil) // Then - XCTAssertEqual(writer.records.count, 1) + XCTAssertEqual(recordWriter.records.count, 1) - let enrichedRecord = try XCTUnwrap(writer.records.first) + let enrichedRecord = try XCTUnwrap(recordWriter.records.first) XCTAssertEqual(enrichedRecord.applicationID, rum.applicationID) XCTAssertEqual(enrichedRecord.sessionID, rum.sessionID) XCTAssertEqual(enrichedRecord.viewID, rum.viewID) @@ -61,7 +66,12 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor( + queue: NoQueue(), + recordWriter: recordWriter, + srContextPublisher: srContextPublisher, + telemetry: TelemetryMock() + ) let viewTree = generateSimpleViewTree() // When @@ -74,8 +84,8 @@ class ProcessorTests: XCTestCase { processor.process(viewTreeSnapshot: snapshot3, touchSnapshot: nil) // Then - let enrichedRecords = writer.records - XCTAssertEqual(writer.records.count, 3) + let enrichedRecords = recordWriter.records + XCTAssertEqual(recordWriter.records.count, 3) XCTAssertEqual(enrichedRecords[0].records.count, 3, "Segment must start with 'meta' → 'focus' → 'full snapshot' records") XCTAssertTrue(enrichedRecords[0].records[0].isMetaRecord) @@ -108,7 +118,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), recordWriter: recordWriter, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let view = UIView.mock(withFixture: .visible(.someAppearance)) view.frame = CGRect(x: 0, y: 0, width: 100, height: 200) let rotatedView = UIView.mock(withFixture: .visible(.someAppearance)) @@ -122,8 +132,8 @@ class ProcessorTests: XCTestCase { processor.process(viewTreeSnapshot: snapshot2, touchSnapshot: nil) // Then - let enrichedRecords = writer.records - XCTAssertEqual(writer.records.count, 2) + let enrichedRecords = recordWriter.records + XCTAssertEqual(recordWriter.records.count, 2) XCTAssertEqual(enrichedRecords[0].records.count, 3, "Segment must start with 'meta' → 'focus' → 'full snapshot' records") XCTAssertTrue(enrichedRecords[0].records[0].isMetaRecord) @@ -147,7 +157,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), recordWriter: recordWriter, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When @@ -162,8 +172,8 @@ class ProcessorTests: XCTestCase { processor.process(viewTreeSnapshot: snapshot4, touchSnapshot: nil) // Then - let enrichedRecords = writer.records - XCTAssertEqual(writer.records.count, 4) + let enrichedRecords = recordWriter.records + XCTAssertEqual(recordWriter.records.count, 4) XCTAssertEqual(enrichedRecords[0].records.count, 3, "Segment must start with 'meta' → 'focus' → 'full snapshot' records") XCTAssertTrue(enrichedRecords[0].records[0].isMetaRecord) @@ -201,16 +211,16 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), recordWriter: recordWriter, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) // When let touchSnapshot = generateTouchSnapshot(startAt: earliestTouchTime, endAt: snapshotTime, numberOfTouches: numberOfTouches) processor.process(viewTreeSnapshot: .mockWith(date: snapshotTime, context: .mockWith(rumContext: rum)), touchSnapshot: touchSnapshot) // Then - XCTAssertEqual(writer.records.count, 1) + XCTAssertEqual(recordWriter.records.count, 1) - let enrichedRecord = try XCTUnwrap(writer.records.first) + let enrichedRecord = try XCTUnwrap(recordWriter.records.first) XCTAssertEqual(enrichedRecord.applicationID, rum.applicationID) XCTAssertEqual(enrichedRecord.sessionID, rum.sessionID) XCTAssertEqual(enrichedRecord.viewID, rum.viewID) @@ -246,7 +256,7 @@ class ProcessorTests: XCTestCase { // Given let core = PassthroughCoreMock() let srContextPublisher = SRContextPublisher(core: core) - let processor = SnapshotProcessor(queue: NoQueue(), writer: writer, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) + let processor = SnapshotProcessor(queue: NoQueue(), recordWriter: recordWriter, srContextPublisher: srContextPublisher, telemetry: TelemetryMock()) let viewTree = generateSimpleViewTree() // When @@ -257,8 +267,8 @@ class ProcessorTests: XCTestCase { processor.process(viewTreeSnapshot: snapshot2, touchSnapshot: nil) // Then - let enrichedRecords = writer.records - XCTAssertEqual(writer.records.count, 2) + let enrichedRecords = recordWriter.records + XCTAssertEqual(recordWriter.records.count, 2) XCTAssertEqual(enrichedRecords[0].records.count, 3, "Segment must start with 'meta' → 'focus' → 'full snapshot' records") XCTAssertTrue(enrichedRecords[0].records[0].isMetaRecord) diff --git a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift index 1365f467f0..9d39a7a8b8 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift @@ -13,22 +13,24 @@ class RecorderTests: XCTestCase { func testAfterCapturingSnapshot_itIsPassesToProcessor() { let mockViewTreeSnapshots: [ViewTreeSnapshot] = .mockRandom(count: 1) let mockTouchSnapshots: [TouchSnapshot] = .mockRandom(count: 1) - let processor = ProcessorSpy() + let snapshotProcessor = SnapshotProcessorSpy() + let resourceProcessor = ResourceProcessorSpy() // Given let recorder = Recorder( uiApplicationSwizzler: .mockAny(), viewTreeSnapshotProducer: ViewTreeSnapshotProducerMock(succeedingSnapshots: mockViewTreeSnapshots), touchSnapshotProducer: TouchSnapshotProducerMock(succeedingSnapshots: mockTouchSnapshots), - snapshotProcessor: processor, + snapshotProcessor: snapshotProcessor, + resourceProcessor: resourceProcessor, telemetry: TelemetryMock() ) // When recorder.captureNextRecord(.mockRandom()) // Then - DDAssertReflectionEqual(processor.processedSnapshots.map { $0.viewTreeSnapshot }, mockViewTreeSnapshots) - DDAssertReflectionEqual(processor.processedSnapshots.map { $0.touchSnapshot }, mockTouchSnapshots) + DDAssertReflectionEqual(snapshotProcessor.processedSnapshots.map { $0.viewTreeSnapshot }, mockViewTreeSnapshots) + DDAssertReflectionEqual(snapshotProcessor.processedSnapshots.map { $0.touchSnapshot }, mockTouchSnapshots) } func testWhenCapturingSnapshots_itUsesDefaultRecorderContext() { @@ -41,7 +43,8 @@ class RecorderTests: XCTestCase { uiApplicationSwizzler: .mockAny(), viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, - snapshotProcessor: ProcessorSpy(), + snapshotProcessor: SnapshotProcessorSpy(), + resourceProcessor: ResourceProcessorSpy(), telemetry: TelemetryMock() ) // When @@ -63,7 +66,8 @@ class RecorderTests: XCTestCase { uiApplicationSwizzler: .mockAny(), viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: TouchSnapshotProducerMock(), - snapshotProcessor: ProcessorSpy(), + snapshotProcessor: SnapshotProcessorSpy(), + resourceProcessor: ResourceProcessorSpy(), telemetry: telemetry ) @@ -95,7 +99,8 @@ class RecorderTests: XCTestCase { uiApplicationSwizzler: .mockAny(), viewTreeSnapshotProducer: viewTreeSnapshotProducer, touchSnapshotProducer: touchSnapshotProducer, - snapshotProcessor: ProcessorSpy(), + snapshotProcessor: SnapshotProcessorSpy(), + resourceProcessor: ResourceProcessorSpy(), telemetry: TelemetryMock() ) // When diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index df4d268b33..3bf3d40438 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -17,11 +17,14 @@ private struct MockSemantics: NodeSemantics { var nodes: [Node] var resources: [Resource] - init(subtreeStrategy: NodeSubtreeStrategy, nodeNames: [String]) { + init(subtreeStrategy: NodeSubtreeStrategy, nodeNames: [String], resourcesIdentifiers: [String]) { self.subtreeStrategy = subtreeStrategy self.nodes = nodeNames.map { Node(viewAttributes: .mockAny(), wireframesBuilder: MockWireframesBuilder(nodeName: $0)) } + self.resources = resourcesIdentifiers.map { + MockResource(identifier: $0, data: .mockRandom()) + } } } @@ -60,7 +63,6 @@ class ViewTreeRecorderTests: XCTestCase { func testItQueriesNodeRecordersUntilOneFindsBestSemantics() { // Given let view = UIView(frame: .mockRandom()) - Node.mockAny() let unknownElement = UnknownElement.constant let ambiguousElement = AmbiguousElement(nodes: .mockAny(), resources: .mockAny()) let specificElement = SpecificElement(subtreeStrategy: .mockRandom(), nodes: .mockAny()) @@ -117,22 +119,22 @@ class ViewTreeRecorderTests: XCTestCase { c.addSubview(cb) let semanticsByView: [UIView: NodeSemantics] = [ - rootView: MockSemantics(subtreeStrategy: .record, nodeNames: ["rootView"]), - a: MockSemantics(subtreeStrategy: .record, nodeNames: ["a"]), - b: MockSemantics(subtreeStrategy: .record, nodeNames: ["b"]), - c: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["c"]), // ignore subtree of `c` - aa: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["aa", "aav1", "aav2", "aav3"]), // replace `aaa` (subtree of `aa`) with 3 nodes - ab: MockSemantics(subtreeStrategy: .record, nodeNames: ["ab"]), - aba: MockSemantics(subtreeStrategy: .record, nodeNames: ["aba"]), - abb: MockSemantics(subtreeStrategy: .record, nodeNames: ["abb"]), - ca: MockSemantics(subtreeStrategy: .record, nodeNames: ["ca"]), - cb: MockSemantics(subtreeStrategy: .record, nodeNames: ["cb"]), + rootView: MockSemantics(subtreeStrategy: .record, nodeNames: ["rootView"], resourcesIdentifiers: []), + a: MockSemantics(subtreeStrategy: .record, nodeNames: ["a"], resourcesIdentifiers: []), + b: MockSemantics(subtreeStrategy: .record, nodeNames: ["b"], resourcesIdentifiers: []), + c: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["c"], resourcesIdentifiers: []), // ignore subtree of `c` + aa: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["aa", "aav1", "aav2", "aav3"], resourcesIdentifiers: []), // replace `aaa` (subtree of `aa`) with 3 nodes + ab: MockSemantics(subtreeStrategy: .record, nodeNames: ["ab"], resourcesIdentifiers: []), + aba: MockSemantics(subtreeStrategy: .record, nodeNames: ["aba"], resourcesIdentifiers: []), + abb: MockSemantics(subtreeStrategy: .record, nodeNames: ["abb"], resourcesIdentifiers: []), + ca: MockSemantics(subtreeStrategy: .record, nodeNames: ["ca"], resourcesIdentifiers: []), + cb: MockSemantics(subtreeStrategy: .record, nodeNames: ["cb"], resourcesIdentifiers: []), ] // When let nodeRecorder = NodeRecorderMock(resultForView: { view in semanticsByView[view] }) let recorder = ViewTreeRecorder(nodeRecorders: [nodeRecorder]) - let nodes = recorder.record(rootView, in: .mockRandom()) + let nodes = recorder.record(rootView, in: .mockRandom()).nodes // Then let expectedNodes = ["rootView", "a", "aa", "aav1", "aav2", "aav3", "ab", "aba", "abb", "b", "c"] @@ -162,7 +164,7 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.record(view, in: .mockRandom()) + let nodes = recorder.record(view, in: .mockRandom()).nodes // Then XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for \(type(of: view)) when it is not visible") @@ -180,19 +182,19 @@ class ViewTreeRecorderTests: XCTestCase { let `switch` = UISwitch.mock(withFixture: .visible(.noAppearance)) // When - let viewNodes = recorder.record(view, in: .mockRandom()) + let viewNodes = recorder.record(view, in: .mockRandom()).nodes XCTAssertTrue(viewNodes.isEmpty, "No nodes should be recorded for `UIView` when it has no appearance") - let labelNodes = recorder.record(label, in: .mockRandom()) + let labelNodes = recorder.record(label, in: .mockRandom()).nodes XCTAssertTrue(labelNodes.isEmpty, "No nodes should be recorded for `UILabel` when it has no appearance") - let imageViewNodes = recorder.record(imageView, in: .mockRandom()) + let imageViewNodes = recorder.record(imageView, in: .mockRandom()).nodes XCTAssertTrue(imageViewNodes.isEmpty, "No nodes should be recorded for `UIImageView` when it has no appearance") - let textFieldNodes = recorder.record(textField, in: .mockRandom()) + let textFieldNodes = recorder.record(textField, in: .mockRandom()).nodes XCTAssertTrue(textFieldNodes.isEmpty, "No nodes should be recorded for `UITextField` when it has no appearance") - let switchNodes = recorder.record(`switch`, in: .mockRandom()) + let switchNodes = recorder.record(`switch`, in: .mockRandom()).nodes XCTAssertFalse( switchNodes.isEmpty, "`UISwitch` with no appearance should record some nodes as it has style coming from its internal subtree." @@ -214,7 +216,7 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.record(view, in: .mockRandom()) + let nodes = recorder.record(view, in: .mockRandom()).nodes // Then XCTAssertFalse(nodes.isEmpty, "Some nodes should be recorded for \(type(of: view)) when it has some appearance") diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift index c1b07e3abe..a3c78cee5e 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift @@ -181,7 +181,7 @@ class NodeSemanticsTests: XCTestCase { func testImportance() { let unknownElement = UnknownElement.constant let invisibleElement = InvisibleElement.constant - let ambiguousElement = AmbiguousElement(nodes: []) + let ambiguousElement = AmbiguousElement(nodes: [], resources: []) let specificElement = SpecificElement(subtreeStrategy: .mockAny(), nodes: []) XCTAssertGreaterThan( From 779ac9d8d3baec043c9ad4f1dad822db9bfea85c Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 14 Dec 2023 11:00:46 +0000 Subject: [PATCH 05/60] RUM-2153 Clean up --- .../Tests/Datadog/RUM/RUMDebuggingTests.swift | 10 +++++----- .../Sources/Feature/SessionReplayFeature.swift | 17 ++++++----------- .../Sources/Processor/ResourcesProcessor.swift | 5 +---- .../NodeRecorders/UIImageViewRecorder.swift | 2 +- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 2 +- .../Sources/Writers/RecordWriter.swift | 4 ++-- .../Sources/Writers/ResourcesWriter.swift | 4 ++-- .../Tests/Mocks/RecorderMocks.swift | 6 +++--- .../Tests/Mocks/ResourceProcessorSpy.swift | 1 - .../Processor/ResourceProcessorTests.swift | 3 +-- 10 files changed, 22 insertions(+), 32 deletions(-) diff --git a/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift index 84daabfcdf..b3ca57449d 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMDebuggingTests.swift @@ -24,7 +24,7 @@ class RUMDebuggingTests: XCTestCase { _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "FirstView"), context: .mockAny(), - recordsWriter: FileWriterMock() + writer: FileWriterMock() ) let debugging = RUMDebugging() @@ -47,7 +47,7 @@ class RUMDebuggingTests: XCTestCase { func testWhenOneRUMViewIsInactive_andSecondIsActive_itDisplaysTwoRUMViewOutlines() throws { let context: DatadogContext = .mockAny() - let recordsWriter = FileWriterMock() + let writer = FileWriterMock() let expectation = self.expectation(description: "Render RUMDebugging") @@ -58,17 +58,17 @@ class RUMDebuggingTests: XCTestCase { _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "FirstView"), context: context, - recordsWriter: recordsWriter + writer: writer ) _ = applicationScope.process( command: RUMStartResourceCommand.mockAny(), context: context, - recordsWriter: recordsWriter + writer: writer ) _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockViewIdentity, name: "SecondView"), context: context, - recordsWriter: recordsWriter + writer: writer ) let debugging = RUMDebugging() diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 6d35fb1624..1bc8ae1e88 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -34,20 +34,19 @@ internal class SessionReplayFeature: DatadogRemoteFeature { ) let resourceProcessor = ResourceProcessor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.resource-processor"), - resourcesWriter: ResourcesWriter(core: core), - telemetry: core.telemetry + resourcesWriter: ResourcesWriter(core: core) ) - - let scheduler = MainThreadScheduler(interval: 0.1) - let messageReceiver = RUMContextReceiver() - let recorder = try Recorder( snapshotProcessor: snapshotProcessor, resourceProcessor: resourceProcessor, telemetry: core.telemetry, additionalNodeRecorders: configuration._additionalNodeRecorders ) - let recordingCoordinator = RecordingCoordinator( + let scheduler = MainThreadScheduler(interval: 0.1) + let messageReceiver = RUMContextReceiver() + + self.messageReceiver = messageReceiver + self.recordingCoordinator = RecordingCoordinator( scheduler: scheduler, privacy: configuration.defaultPrivacyLevel, rumContextObserver: messageReceiver, @@ -55,10 +54,6 @@ internal class SessionReplayFeature: DatadogRemoteFeature { recorder: recorder, sampler: Sampler(samplingRate: configuration.debugSDK ? 100 : configuration.replaySampleRate) ) - - self.messageReceiver = messageReceiver - self.recordingCoordinator = recordingCoordinator - self.requestBuilder = SegmentRequestBuilder( customUploadURL: configuration.customEndpoint, telemetry: core.telemetry diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift index bc3fa61c44..df5abbd5d0 100644 --- a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift @@ -17,8 +17,6 @@ internal class ResourceProcessor: ResourceProcessing { private let queue: Queue /// Writes records to `DatadogCore`. private let resourcesWriter: ResourcesWriting - /// Sends telemetry through sdk core. - private let telemetry: Telemetry func process(resources: [Resource], context: EnrichedResource.Context) { queue.run { [resourcesWriter] in @@ -26,10 +24,9 @@ internal class ResourceProcessor: ResourceProcessing { } } - init(queue: Queue, resourcesWriter: ResourcesWriting, telemetry: Telemetry) { + init(queue: Queue, resourcesWriter: ResourcesWriting) { self.queue = queue self.resourcesWriter = resourcesWriter - self.telemetry = telemetry } } #endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 3679c70343..8691f9cc0c 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -8,7 +8,7 @@ import UIKit @_spi(Internal) -extension UIImage: SessionReplayResource { +extension UIImage: Resource { public var identifier: String { return srIdentifier } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 19a7508d96..1a134ff966 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -26,7 +26,7 @@ internal struct ViewTreeSnapshot { /// An array of nodes recorded for this snapshot - sequenced in DFS order. let nodes: [Node] /// An array of resource references recorded for this snapshot - sequenced in DFS order. - /// May contain references to the same resource if it appears multiple times in the snapshot. + /// May contain multiple references to the same resource, if it appears multiple times in the snapshot. let resources: [Resource] } diff --git a/DatadogSessionReplay/Sources/Writers/RecordWriter.swift b/DatadogSessionReplay/Sources/Writers/RecordWriter.swift index 9f6d7af060..42923bf279 100644 --- a/DatadogSessionReplay/Sources/Writers/RecordWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/RecordWriter.swift @@ -42,8 +42,8 @@ internal class RecordWriter: RecordWriting { return } - scope.eventWriteContext(bypassConsent: false, forceNewBatch: forceNewBatch) { _, recordsWriter in - recordsWriter.write(value: nextRecord) + scope.eventWriteContext(bypassConsent: false, forceNewBatch: forceNewBatch) { _, recordWriter in + recordWriter.write(value: nextRecord) } } } diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index 4a2be5719c..a195ce5c43 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -30,8 +30,8 @@ internal class ResourcesWriter: ResourcesWriting { guard let scope = core?.scope(for: ResourcesFeature.name) else { return } - scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, recordsWriter in - recordsWriter.write(value: resources) + scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, recordWriter in + recordWriter.write(value: resources) } } } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index 6c022d715a..d3ee40086e 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -274,11 +274,11 @@ struct MockResource: Resource, AnyMockable, RandomMockable { self.data = data } - static func mockAny() -> MockResource { - return MockResource(identifier: .mockAny(), data: .mockAny()) + static func mockAny() -> Self { + return .init(identifier: .mockAny(), data: .mockAny()) } - static func mockRandom() -> MockResource { + static func mockRandom() -> Self { return MockResource(identifier: . mockRandom(), data: .mockRandom()) } } diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift index ff46c917dc..57dc0ca7da 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift @@ -14,5 +14,4 @@ internal class ResourceProcessorSpy: ResourceProcessing { func process(resources: [Resource], context: EnrichedResource.Context) { processedResources.append((resources, context)) } - } diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift index 6e2df458ee..dc76376dc8 100644 --- a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -24,8 +24,7 @@ class ResourceProcessorTests: XCTestCase { let writer = ResourceWriterMock() let processor = ResourceProcessor( queue: NoQueue(), - resourcesWriter: writer, - telemetry: NOPTelemetry() + resourcesWriter: writer ) let resource1: MockResource = .mockAny() From eced416bce7a1e79534cdfe8aa4e8d281d304470 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 14 Dec 2023 12:27:27 +0000 Subject: [PATCH 06/60] RUM-2153 Improve coverage --- .../Feature/SessionReplayFeature.swift | 2 +- .../Processor/ResourcesProcessor.swift | 2 - .../Sources/Recorder/Recorder.swift | 10 +-- .../NodeRecorders/UIImageViewRecorder.swift | 9 +- .../Tests/Mocks/RecorderMocks.swift | 10 +-- .../Tests/Mocks/ResourceMocks.swift | 2 +- .../Tests/Mocks/ResourceProcessorSpy.swift | 4 +- .../Processor/ResourceProcessorTests.swift | 27 +++++- .../Tests/Recorder/RecorderTests.swift | 6 +- .../UIImageViewRecorderTests.swift | 18 ++++ .../ViewTreeRecorderTests.swift | 84 ++++++++++++------- 11 files changed, 120 insertions(+), 54 deletions(-) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 1bc8ae1e88..c473663661 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -10,7 +10,7 @@ import DatadogInternal internal class SessionReplayFeature: DatadogRemoteFeature { static let name: String = "session-replay" - + let requestBuilder: FeatureRequestBuilder let messageReceiver: FeatureMessageReceiver let performanceOverride: PerformancePresetOverride? diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift index df5abbd5d0..28428520b5 100644 --- a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift @@ -13,9 +13,7 @@ internal protocol ResourceProcessing { } internal class ResourceProcessor: ResourceProcessing { - /// The background queue for executing all logic. private let queue: Queue - /// Writes records to `DatadogCore`. private let resourcesWriter: ResourcesWriting func process(resources: [Resource], context: EnrichedResource.Context) { diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index 30e8e5a60b..dae35d1a02 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -124,12 +124,10 @@ public class Recorder: Recording { let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext) snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot) - if !viewTreeSnapshot.resources.isEmpty { - resourceProcessor.process( - resources: viewTreeSnapshot.resources, - context: .init(recorderContext.applicationID) - ) - } + resourceProcessor.process( + resources: viewTreeSnapshot.resources, + context: .init(recorderContext.applicationID) + ) } catch let error { telemetry.error("[SR] Failed to take snapshot", error: DDError(error: error)) } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 8691f9cc0c..bdaf396032 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,7 +7,6 @@ #if os(iOS) import UIKit @_spi(Internal) - extension UIImage: Resource { public var identifier: String { return srIdentifier @@ -88,12 +87,14 @@ internal struct UIImageViewRecorder: NodeRecorder { return SpecificElement( subtreeStrategy: .record, nodes: [node], - resources: [imageView.image].filter { image in + resources: [imageView.image] + .filter { image in defer { - image?.recorded = true + image?.recorded = shouldRecordImage } return image?.recorded == false && shouldRecordImage - }.compactMap { $0 } + } + .compactMap { $0 } ) } } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index d3ee40086e..56eae2605c 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -33,7 +33,7 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { date: .mockRandom(), context: .mockRandom(), viewportSize: .mockRandom(), - nodes: .mockRandom(count: .random(in: (5..<50))), + nodes: .mockRandom(count: .random(in: (5..<50))), resources: .mockRandom(count: .random(in: (5..<50))) ) } @@ -49,7 +49,7 @@ extension ViewTreeSnapshot: AnyMockable, RandomMockable { date: date, context: context, viewportSize: viewportSize, - nodes: nodes, + nodes: nodes, resources: resources ) } @@ -274,11 +274,11 @@ struct MockResource: Resource, AnyMockable, RandomMockable { self.data = data } - static func mockAny() -> Self { - return .init(identifier: .mockAny(), data: .mockAny()) + static func mockAny() -> MockResource { + return MockResource(identifier: .mockAny(), data: .mockAny()) } - static func mockRandom() -> Self { + static func mockRandom() -> MockResource { return MockResource(identifier: . mockRandom(), data: .mockRandom()) } } diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift index 8b7c937d79..e178fd3a9c 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceMocks.swift @@ -32,7 +32,7 @@ extension EnrichedResource.Context: RandomMockable, AnyMockable { .mockAny() ) } - + public static func mockRandom() -> Self { return .init( .mockRandom() diff --git a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift index 57dc0ca7da..ffe7659fba 100644 --- a/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift +++ b/DatadogSessionReplay/Tests/Mocks/ResourceProcessorSpy.swift @@ -9,9 +9,9 @@ import Foundation /// Spies the interaction with `Processing`. internal class ResourceProcessorSpy: ResourceProcessing { - var processedResources: [([Resource], EnrichedResource.Context)] = [] + var processedResources: [(resources: [Resource], context: EnrichedResource.Context)] = [] func process(resources: [Resource], context: EnrichedResource.Context) { - processedResources.append((resources, context)) + processedResources.append((resources: resources, context: context)) } } diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift index dc76376dc8..a5a8305e5b 100644 --- a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -27,9 +27,9 @@ class ResourceProcessorTests: XCTestCase { resourcesWriter: writer ) - let resource1: MockResource = .mockAny() - let resource2: MockResource = .mockAny() - let context: EnrichedResource.Context = .mockAny() + let resource1: MockResource = .mockRandom() + let resource2: MockResource = .mockRandom() + let context: EnrichedResource.Context = .mockRandom() processor.process(resources: [resource1, resource2], context: context) @@ -42,4 +42,25 @@ class ResourceProcessorTests: XCTestCase { ]) ) } + + func testItRemovesDuplicateResources() { + let writer = ResourceWriterMock() + let processor = ResourceProcessor( + queue: NoQueue(), + resourcesWriter: writer + ) + + let resource: MockResource = .mockRandom() + let context: EnrichedResource.Context = .mockRandom() + + processor.process(resources: [resource, resource], context: context) + + XCTAssertEqual(writer.resources.count, 1) + XCTAssertEqual( + writer.resources[0], + Set([ + EnrichedResource(resource: resource, context: context) + ]) + ) + } } diff --git a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift index 9d39a7a8b8..86f37a3e6a 100644 --- a/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/RecorderTests.swift @@ -25,12 +25,16 @@ class RecorderTests: XCTestCase { resourceProcessor: resourceProcessor, telemetry: TelemetryMock() ) + let recorderContext = Recorder.Context.mockRandom() + // When - recorder.captureNextRecord(.mockRandom()) + recorder.captureNextRecord(recorderContext) // Then DDAssertReflectionEqual(snapshotProcessor.processedSnapshots.map { $0.viewTreeSnapshot }, mockViewTreeSnapshots) DDAssertReflectionEqual(snapshotProcessor.processedSnapshots.map { $0.touchSnapshot }, mockTouchSnapshots) + DDAssertReflectionEqual(resourceProcessor.processedResources.map { $0.resources }, mockViewTreeSnapshots.map { $0.resources }) + DDAssertReflectionEqual(resourceProcessor.processedResources.map { $0.context }, mockViewTreeSnapshots.map { _ in EnrichedResource.Context(recorderContext.applicationID) }) } func testWhenCapturingSnapshots_itUsesDefaultRecorderContext() { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift index 727176a7de..e60fe32613 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift @@ -64,6 +64,24 @@ class UIImageViewRecorderTests: XCTestCase { XCTAssertEqual(semantics.subtreeStrategy, .record, "Image view's subtree should be recorded") let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIImageViewWireframesBuilder) XCTAssertFalse(builder.shouldRecordImage) + XCTAssertNil(semantics.resources.first as? UIImage) + XCTAssertEqual(imageView.image?.recorded, false) + } + + func testWhenShouldRecordImagePredicateReturnsTrue() throws { + // When + let recorder = UIImageViewRecorder(shouldRecordImagePredicate: { _ in return true }) + 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) + XCTAssertTrue(builder.shouldRecordImage) + XCTAssertEqual(semantics.resources.first as? UIImage, imageView.image) + XCTAssertEqual(imageView.image?.recorded, true) } func testWhenTintColorIsProvided() throws { diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index 3bf3d40438..18fba2d362 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -88,7 +88,7 @@ class ViewTreeRecorderTests: XCTestCase { // MARK: - Recording Nodes Recursively - func testItQueriesViewTreeRecursivelyAndReturnsNodesInDFSOrder() { + func testItQueriesViewTreeRecursivelyAndReturnsNodesAndResourcesInDFSOrder() { // Given /* @@ -119,28 +119,34 @@ class ViewTreeRecorderTests: XCTestCase { c.addSubview(cb) let semanticsByView: [UIView: NodeSemantics] = [ - rootView: MockSemantics(subtreeStrategy: .record, nodeNames: ["rootView"], resourcesIdentifiers: []), - a: MockSemantics(subtreeStrategy: .record, nodeNames: ["a"], resourcesIdentifiers: []), - b: MockSemantics(subtreeStrategy: .record, nodeNames: ["b"], resourcesIdentifiers: []), - c: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["c"], resourcesIdentifiers: []), // ignore subtree of `c` - aa: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["aa", "aav1", "aav2", "aav3"], resourcesIdentifiers: []), // replace `aaa` (subtree of `aa`) with 3 nodes - ab: MockSemantics(subtreeStrategy: .record, nodeNames: ["ab"], resourcesIdentifiers: []), - aba: MockSemantics(subtreeStrategy: .record, nodeNames: ["aba"], resourcesIdentifiers: []), - abb: MockSemantics(subtreeStrategy: .record, nodeNames: ["abb"], resourcesIdentifiers: []), - ca: MockSemantics(subtreeStrategy: .record, nodeNames: ["ca"], resourcesIdentifiers: []), - cb: MockSemantics(subtreeStrategy: .record, nodeNames: ["cb"], resourcesIdentifiers: []), + rootView: MockSemantics(subtreeStrategy: .record, nodeNames: ["rootView"], resourcesIdentifiers: ["rootResource"]), + a: MockSemantics(subtreeStrategy: .record, nodeNames: ["a"], resourcesIdentifiers: ["a"]), + b: MockSemantics(subtreeStrategy: .record, nodeNames: ["b"], resourcesIdentifiers: ["b"]), + c: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["c"], resourcesIdentifiers: ["c"]), // ignore subtree of `c` + aa: MockSemantics(subtreeStrategy: .ignore, nodeNames: ["aa", "aav1", "aav2", "aav3"], resourcesIdentifiers: ["aa", "aav1", "aav2", "aav3"]), // replace `aaa` (subtree of `aa`) with 3 nodes + ab: MockSemantics(subtreeStrategy: .record, nodeNames: ["ab"], resourcesIdentifiers: ["ab"]), + aba: MockSemantics(subtreeStrategy: .record, nodeNames: ["aba"], resourcesIdentifiers: ["aba"]), + abb: MockSemantics(subtreeStrategy: .record, nodeNames: ["abb"], resourcesIdentifiers: ["abb"]), + ca: MockSemantics(subtreeStrategy: .record, nodeNames: ["ca"], resourcesIdentifiers: ["ca"]), + cb: MockSemantics(subtreeStrategy: .record, nodeNames: ["cb"], resourcesIdentifiers: ["cb"]), ] // When let nodeRecorder = NodeRecorderMock(resultForView: { view in semanticsByView[view] }) let recorder = ViewTreeRecorder(nodeRecorders: [nodeRecorder]) - let nodes = recorder.record(rootView, in: .mockRandom()).nodes + let recordingResult = recorder.record(rootView, in: .mockRandom()) + let nodes = recordingResult.nodes + let resources = recordingResult.resources // Then let expectedNodes = ["rootView", "a", "aa", "aav1", "aav2", "aav3", "ab", "aba", "abb", "b", "c"] let actualNodes = nodes.compactMap { ($0.wireframesBuilder as? MockWireframesBuilder)?.nodeName } XCTAssertEqual(expectedNodes, actualNodes, "Nodes must be recorded in DFS order") + let expectedResources = ["rootResource", "a", "aa", "aav1", "aav2", "aav3", "ab", "aba", "abb", "b", "c"] + let actualResources = resources.map { $0.identifier } + XCTAssertEqual(expectedResources, actualResources, "Resources must be recorded in DFS order") + let expectedQueriedViews: [UIView] = [rootView, a, b, c, aa, ab, aba, abb] XCTAssertEqual(nodeRecorder.queriedViews.count, expectedQueriedViews.count) expectedQueriedViews.forEach { XCTAssertTrue(nodeRecorder.queriedViews.contains($0)) } @@ -164,10 +170,12 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.record(view, in: .mockRandom()).nodes - + let recordingResult = recorder.record(view, in: .mockRandom()) + let nodes = recordingResult.nodes + let resources = recordingResult.resources // Then XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for \(type(of: view)) when it is not visible") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for \(type(of: view)) when it is not visible") } } @@ -182,23 +190,38 @@ class ViewTreeRecorderTests: XCTestCase { let `switch` = UISwitch.mock(withFixture: .visible(.noAppearance)) // When - let viewNodes = recorder.record(view, in: .mockRandom()).nodes - XCTAssertTrue(viewNodes.isEmpty, "No nodes should be recorded for `UIView` when it has no appearance") - - let labelNodes = recorder.record(label, in: .mockRandom()).nodes - XCTAssertTrue(labelNodes.isEmpty, "No nodes should be recorded for `UILabel` when it has no appearance") - - let imageViewNodes = recorder.record(imageView, in: .mockRandom()).nodes - XCTAssertTrue(imageViewNodes.isEmpty, "No nodes should be recorded for `UIImageView` when it has no appearance") - - let textFieldNodes = recorder.record(textField, in: .mockRandom()).nodes - XCTAssertTrue(textFieldNodes.isEmpty, "No nodes should be recorded for `UITextField` when it has no appearance") - - let switchNodes = recorder.record(`switch`, in: .mockRandom()).nodes + var recordingResult = recorder.record(view, in: .mockRandom()) + var nodes = recordingResult.nodes + var resources = recordingResult.resources + XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UIView` when it has no appearance") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for `UIView` when it has no appearance") + + recordingResult = recorder.record(label, in: .mockRandom()) + nodes = recordingResult.nodes + resources = recordingResult.resources + XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UILabel` when it has no appearance") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for `UILabel` when it has no appearance") + + recordingResult = recorder.record(imageView, in: .mockRandom()) + nodes = recordingResult.nodes + resources = recordingResult.resources + XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UIImageView` when it has no appearance") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for `UIImageView` when it has no appearance") + + recordingResult = recorder.record(textField, in: .mockRandom()) + nodes = recordingResult.nodes + resources = recordingResult.resources + XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UITextField` when it has no appearance") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for `UITextField` when it has no appearance") + + recordingResult = recorder.record(`switch`, in: .mockRandom()) + nodes = recordingResult.nodes + resources = recordingResult.resources XCTAssertFalse( - switchNodes.isEmpty, + nodes.isEmpty, "`UISwitch` with no appearance should record some nodes as it has style coming from its internal subtree." ) + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for `UISwitch` when it has no appearance") } func testItRecordsViewsWithSomeAppearance() { @@ -216,10 +239,13 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.record(view, in: .mockRandom()).nodes + let recordingResults = recorder.record(view, in: .mockRandom()) + let nodes = recordingResults.nodes + let resources = recordingResults.resources // Then XCTAssertFalse(nodes.isEmpty, "Some nodes should be recorded for \(type(of: view)) when it has some appearance") + XCTAssertTrue(resources.isEmpty, "No resources should be recorded for \(type(of: view)) regardless it has some appearance") } } From 4b79b5614bf1b791c0fffb724f5b4b69e76ca94c Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 14 Dec 2023 13:52:18 +0000 Subject: [PATCH 07/60] RUM-2153 Remove unnecessary deduplication --- .../Sources/Models/EnrichedResource.swift | 6 +---- .../Processor/ResourcesProcessor.swift | 5 +++- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 1 - .../Sources/Writers/ResourcesWriter.swift | 4 ++-- .../Processor/ResourceProcessorTests.swift | 23 ++++++------------- 5 files changed, 14 insertions(+), 25 deletions(-) diff --git a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift index 5ded938b62..c38b5d95f3 100644 --- a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift +++ b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift @@ -8,7 +8,7 @@ import Foundation /// Extends the resource information with context. -internal struct EnrichedResource: Hashable, Codable, Resource { +internal struct EnrichedResource: Codable, Resource, Equatable { internal struct Context: Codable, Equatable { internal struct Application: Codable, Equatable { let id: String @@ -38,9 +38,5 @@ internal struct EnrichedResource: Hashable, Codable, Resource { self.data = data self.context = context } - - func hash(into hasher: inout Hasher) { - hasher.combine(identifier) - } } #endif diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift index 28428520b5..e35ac31943 100644 --- a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift @@ -17,8 +17,11 @@ internal class ResourceProcessor: ResourceProcessing { private let resourcesWriter: ResourcesWriting func process(resources: [Resource], context: EnrichedResource.Context) { + guard !resources.isEmpty else { + return + } queue.run { [resourcesWriter] in - resourcesWriter.write(resources: Set(resources.map { EnrichedResource(resource: $0, context: context) })) + resourcesWriter.write(resources: resources.map { EnrichedResource(resource: $0, context: context) }) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 1a134ff966..ad321899db 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -26,7 +26,6 @@ internal struct ViewTreeSnapshot { /// An array of nodes recorded for this snapshot - sequenced in DFS order. let nodes: [Node] /// An array of resource references recorded for this snapshot - sequenced in DFS order. - /// May contain multiple references to the same resource, if it appears multiple times in the snapshot. let resources: [Resource] } diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index a195ce5c43..9d4d201e83 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -11,7 +11,7 @@ import DatadogInternal /// A type writing Session Replay records to `DatadogCore`. internal protocol ResourcesWriting { /// Writes next records to SDK core. - func write(resources: Set) + func write(resources: [EnrichedResource]) } internal class ResourcesWriter: ResourcesWriting { @@ -26,7 +26,7 @@ internal class ResourcesWriter: ResourcesWriting { // MARK: - Writing - func write(resources: Set) { + func write(resources: [EnrichedResource]) { guard let scope = core?.scope(for: ResourcesFeature.name) else { return } diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift index a5a8305e5b..73c683088d 100644 --- a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -12,9 +12,9 @@ import TestUtilities @testable import DatadogSessionReplay private class ResourceWriterMock: ResourcesWriting { - var resources: [Set] = [] + var resources: [[EnrichedResource]] = [] - func write(resources: Set) { + func write(resources: [EnrichedResource]) { self.resources.append(resources) } } @@ -36,31 +36,22 @@ class ResourceProcessorTests: XCTestCase { XCTAssertEqual(writer.resources.count, 1) XCTAssertEqual( writer.resources[0], - Set([ + [ EnrichedResource(resource: resource1, context: context), EnrichedResource(resource: resource2, context: context) - ]) + ] ) } - func testItRemovesDuplicateResources() { + func testItDoesNotTryToWriteEmptyResources() { let writer = ResourceWriterMock() let processor = ResourceProcessor( queue: NoQueue(), resourcesWriter: writer ) - let resource: MockResource = .mockRandom() - let context: EnrichedResource.Context = .mockRandom() - - processor.process(resources: [resource, resource], context: context) + processor.process(resources: [], context: .mockRandom()) - XCTAssertEqual(writer.resources.count, 1) - XCTAssertEqual( - writer.resources[0], - Set([ - EnrichedResource(resource: resource, context: context) - ]) - ) + XCTAssertTrue(writer.resources.isEmpty) } } From 29dff49c140bd503305ce550aa8e8f02f1967fb3 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 14 Dec 2023 14:03:12 +0000 Subject: [PATCH 08/60] RUM-2153 Disable feature --- .../Sources/Feature/SessionReplayFeature.swift | 6 ++++-- DatadogSessionReplay/Sources/Recorder/Recorder.swift | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index c473663661..af60c41648 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -32,13 +32,15 @@ internal class SessionReplayFeature: DatadogRemoteFeature { srContextPublisher: SRContextPublisher(core: core), telemetry: core.telemetry ) - let resourceProcessor = ResourceProcessor( + // RUM-2154 Disabled until prod backend is ready + _ = ResourceProcessor( queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.resource-processor"), resourcesWriter: ResourcesWriter(core: core) ) let recorder = try Recorder( + snapshotProcessor: snapshotProcessor, - resourceProcessor: resourceProcessor, + resourceProcessor: nil, telemetry: core.telemetry, additionalNodeRecorders: configuration._additionalNodeRecorders ) diff --git a/DatadogSessionReplay/Sources/Recorder/Recorder.swift b/DatadogSessionReplay/Sources/Recorder/Recorder.swift index dae35d1a02..3f1f42d4da 100644 --- a/DatadogSessionReplay/Sources/Recorder/Recorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/Recorder.swift @@ -61,13 +61,13 @@ public class Recorder: Recording { /// Turns view tree snapshots into data models that will be uploaded to SR BE. private let snapshotProcessor: SnapshotProcessing /// Processes resources on a background thread. - private let resourceProcessor: ResourceProcessing + private let resourceProcessor: ResourceProcessing? // RUM-2154 Optional until prod backend is ready /// Sends telemetry through sdk core. private let telemetry: Telemetry convenience init( snapshotProcessor: SnapshotProcessing, - resourceProcessor: ResourceProcessing, + resourceProcessor: ResourceProcessing?, telemetry: Telemetry, additionalNodeRecorders: [NodeRecorder] ) throws { @@ -95,7 +95,7 @@ public class Recorder: Recording { viewTreeSnapshotProducer: ViewTreeSnapshotProducer, touchSnapshotProducer: TouchSnapshotProducer, snapshotProcessor: SnapshotProcessing, - resourceProcessor: ResourceProcessing, + resourceProcessor: ResourceProcessing?, telemetry: Telemetry ) { self.uiApplicationSwizzler = uiApplicationSwizzler @@ -124,7 +124,7 @@ public class Recorder: Recording { let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext) snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot) - resourceProcessor.process( + resourceProcessor?.process( resources: viewTreeSnapshot.resources, context: .init(recorderContext.applicationID) ) From 7d42c61f573cfdb54db2a179c79fcfe310a1a97c Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 15 Dec 2023 12:38:44 +0000 Subject: [PATCH 09/60] RUM-2153 Add icon tinting --- .../NodeRecorders/UIImageViewRecorder.swift | 31 ++++++++++++++++--- .../Sources/Writers/ResourcesWriter.swift | 4 ++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index bdaf396032..46f6f77d3e 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -6,13 +6,31 @@ #if os(iOS) import UIKit + @_spi(Internal) -extension UIImage: Resource { +public struct UIImageResource: Resource { + let image: UIImage + let tintColor: UIColor? + + init(image: UIImage, tintColor: UIColor?) { + self.image = image + self.tintColor = tintColor + } + public var identifier: String { - return srIdentifier + var identifier = image.srIdentifier + if let tintColorIdentifier = tintColor?.srIdentifier { + identifier += tintColorIdentifier + } + return identifier } + public var data: Data { - return scaledDownToApproximateSize(256.KB) + var image = self.image + if #available(iOS 13.0, *), let tintColor = tintColor { + image = image.withTintColor(tintColor) + } + return image.scaledDownToApproximateSize(512.KB) } } @@ -72,6 +90,7 @@ internal struct UIImageViewRecorder: NodeRecorder { contentFrame = nil } let shouldRecordImage = shouldRecordImagePredicate(imageView) + let tintColor = tintColorProvider(imageView) let builder = UIImageViewWireframesBuilder( wireframeID: ids[0], imageWireframeID: ids[1], @@ -80,7 +99,7 @@ internal struct UIImageViewRecorder: NodeRecorder { clipsToBounds: imageView.clipsToBounds, image: imageView.image, imageDataProvider: context.imageDataProvider, - tintColor: tintColorProvider(imageView), + tintColor: tintColor, shouldRecordImage: shouldRecordImage ) let node = Node(viewAttributes: attributes, wireframesBuilder: builder) @@ -94,7 +113,9 @@ internal struct UIImageViewRecorder: NodeRecorder { } return image?.recorded == false && shouldRecordImage } - .compactMap { $0 } + .compactMap { image in + image.map { UIImageResource(image: $0, tintColor: tintColor) } + } ) } } diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index 9d4d201e83..08c467126f 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -31,7 +31,9 @@ internal class ResourcesWriter: ResourcesWriting { return } scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, recordWriter in - recordWriter.write(value: resources) + resources.forEach { + recordWriter.write(value: $0) + } } } } From ab6e9c5fb3a2c0d60b9eec9fb42089f031781e10 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 15 Dec 2023 12:49:12 +0000 Subject: [PATCH 10/60] RUM-2153 Fix lint --- DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index af60c41648..4a232bca70 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -38,7 +38,6 @@ internal class SessionReplayFeature: DatadogRemoteFeature { resourcesWriter: ResourcesWriter(core: core) ) let recorder = try Recorder( - snapshotProcessor: snapshotProcessor, resourceProcessor: nil, telemetry: core.telemetry, From ff545df212fb57ee6e5862b2fd1bbefe031d879e Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 2 Jan 2024 13:02:41 +0000 Subject: [PATCH 11/60] RUM-2153 Fix tests --- .../NodeRecorders/UIImageViewRecorderTests.swift | 2 +- DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift index e60fe32613..74200a0b3c 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorderTests.swift @@ -80,7 +80,7 @@ class UIImageViewRecorderTests: XCTestCase { XCTAssertEqual(semantics.subtreeStrategy, .record, "Image view's subtree should be recorded") let builder = try XCTUnwrap(semantics.nodes.first?.wireframesBuilder as? UIImageViewWireframesBuilder) XCTAssertTrue(builder.shouldRecordImage) - XCTAssertEqual(semantics.resources.first as? UIImage, imageView.image) + XCTAssertNotNil(semantics.resources.first) XCTAssertEqual(imageView.image?.recorded, true) } diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index e0d8345bf6..16eb97e9ec 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -22,7 +22,7 @@ class ResourcesWriterTests: XCTestCase { writer.write(resources: [.mockRandom()]) writer.write(resources: [.mockRandom()]) - XCTAssertEqual(core.events(ofType: [EnrichedResource].self).count, 3) + XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 3) } func testWhenFeatureScopeIsNotConnected_itDoesNotWriteRecordsToCore() throws { From f2104377813272deb6061f72195076031ec55693 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 3 Jan 2024 09:00:23 +0000 Subject: [PATCH 12/60] RUM-2153 Fix snapshot tests --- .../Utils/SnapshotTestCase.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift index 35b28abc05..634457e604 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift @@ -40,17 +40,26 @@ internal class SnapshotTestCase: XCTestCase { let expectation = self.expectation(description: "Wait for wireframes") // Set up SR recorder: - let processor = Processor( + let snapshotProcessor = SnapshotProcessor( queue: NoQueue(), - writer: RecordWriter(core: PassthroughCoreMock()), + recordWriter: RecordWriter(core: PassthroughCoreMock()), srContextPublisher: SRContextPublisher(core: PassthroughCoreMock()), telemetry: TelemetryMock() ) - let recorder = try Recorder(processor: processor, telemetry: TelemetryMock(), additionalNodeRecorders: []) + let resourceProcessor = ResourceProcessor( + queue: NoQueue(), + resourcesWriter: ResourcesWriter(core: PassthroughCoreMock()) + ) + let recorder = try Recorder( + snapshotProcessor: snapshotProcessor, + resourceProcessor: resourceProcessor, + telemetry: TelemetryMock(), + additionalNodeRecorders: [] + ) // Set up wireframes interception : var wireframes: [SRWireframe]? - processor.interceptWireframes = { + snapshotProcessor.interceptWireframes = { wireframes = $0 expectation.fulfill() } From 07908415fbe42df7d47c242718bdeebc8c10bf11 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 3 Jan 2024 09:12:26 +0000 Subject: [PATCH 13/60] RUM-2153 Simplify threading --- .../Sources/Feature/SessionReplayFeature.swift | 5 +++-- .../Recorder/Utilities/ImageDataProvider.swift | 14 +++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 4a232bca70..e4f1de5d2c 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -26,15 +26,16 @@ internal class SessionReplayFeature: DatadogRemoteFeature { core: DatadogCoreProtocol, configuration: SessionReplay.Configuration ) throws { + let queue = BackgroundAsyncQueue(named: "com.datadoghq.session-replay.processor") let snapshotProcessor = SnapshotProcessor( - queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.snapshot-processor"), + queue: queue, recordWriter: RecordWriter(core: core), srContextPublisher: SRContextPublisher(core: core), telemetry: core.telemetry ) // RUM-2154 Disabled until prod backend is ready _ = ResourceProcessor( - queue: BackgroundAsyncQueue(named: "com.datadoghq.session-replay.resource-processor"), + queue: queue, resourcesWriter: ResourcesWriter(core: core) ) let recorder = try Recorder( diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift index efbe4c8e49..0c4c38e255 100644 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/ImageDataProvider.swift @@ -100,18 +100,14 @@ import CryptoKit private var customHashKey: UInt8 = 11 fileprivate extension UIImage { - private static var associatedObjectQueue = DispatchQueue(label: "com.datadoghq.customHashQueue") - var customHash: String { - return UIImage.associatedObjectQueue.sync { - if let hash = objc_getAssociatedObject(self, &customHashKey) as? String { - return hash - } - - let hash = computeHash() - objc_setAssociatedObject(self, &customHashKey, hash, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + if let hash = objc_getAssociatedObject(self, &customHashKey) as? String { return hash } + + let hash = computeHash() + objc_setAssociatedObject(self, &customHashKey, hash, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return hash } private func computeHash() -> String { From a10796d183cafbe9e4f9f2089da4c0613bb2768b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 9 Jan 2024 11:59:37 +0000 Subject: [PATCH 14/60] RUM-2153 Refactor --- .../Sources/Models/EnrichedResource.swift | 6 +----- .../Sources/Processor/ResourcesProcessor.swift | 10 +++++++++- .../NodeRecorders/UIImageViewRecorder.swift | 11 ++++++----- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 8 ++++---- DatadogSessionReplay/Sources/Utilities/Queue.swift | 6 +++--- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift index c38b5d95f3..070444370d 100644 --- a/DatadogSessionReplay/Sources/Models/EnrichedResource.swift +++ b/DatadogSessionReplay/Sources/Models/EnrichedResource.swift @@ -8,7 +8,7 @@ import Foundation /// Extends the resource information with context. -internal struct EnrichedResource: Codable, Resource, Equatable { +internal struct EnrichedResource: Codable, Equatable { internal struct Context: Codable, Equatable { internal struct Application: Codable, Equatable { let id: String @@ -25,10 +25,6 @@ internal struct EnrichedResource: Codable, Resource, Equatable { internal var data: Data internal var context: Context - internal init(resource: Resource, context: Context) { - self.init(identifier: resource.identifier, data: resource.data, context: context) - } - internal init( identifier: String, data: Data, diff --git a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift index e35ac31943..c47a51e858 100644 --- a/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift +++ b/DatadogSessionReplay/Sources/Processor/ResourcesProcessor.swift @@ -21,7 +21,15 @@ internal class ResourceProcessor: ResourceProcessing { return } queue.run { [resourcesWriter] in - resourcesWriter.write(resources: resources.map { EnrichedResource(resource: $0, context: context) }) + resourcesWriter.write( + resources: resources.map { + EnrichedResource( + identifier: $0.calculateIdentifier(), + data: $0.calculateData(), + context: context + ) + } + ) } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 46f6f77d3e..0d487b7fd8 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,8 +7,7 @@ #if os(iOS) import UIKit -@_spi(Internal) -public struct UIImageResource: Resource { +struct UIImageResource { let image: UIImage let tintColor: UIColor? @@ -16,8 +15,10 @@ public struct UIImageResource: Resource { self.image = image self.tintColor = tintColor } +} - public var identifier: String { +extension UIImageResource: Resource { + func calculateIdentifier() -> String { var identifier = image.srIdentifier if let tintColorIdentifier = tintColor?.srIdentifier { identifier += tintColorIdentifier @@ -25,12 +26,12 @@ public struct UIImageResource: Resource { return identifier } - public var data: Data { + func calculateData() -> Data { var image = self.image if #available(iOS 13.0, *), let tintColor = tintColor { image = image.withTintColor(tintColor) } - return image.scaledDownToApproximateSize(512.KB) + return image.scaledDownToApproximateSize(1.MB) // Intake limit is 10MB - to be adjusted in RUM-2153 } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index ad321899db..78f0c6f444 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -56,10 +56,10 @@ internal typealias Node = SessionReplayNode // An individual resource in `ViewTreeSnapshot`. It is used to describe binary representation of heavy resources such as images. @_spi(Internal) public protocol SessionReplayResource { - /// The unique identifier of the resource. - var identifier: String { get } - /// The data of the resource. - var data: Data { get } + /// Calculates the unique identifier of the resource. + func calculateIdentifier() -> String + /// Calculates the data of the resource. + func calculateData() -> Data } /// This alias enables us to have a more unique name exposed through public-internal access level diff --git a/DatadogSessionReplay/Sources/Utilities/Queue.swift b/DatadogSessionReplay/Sources/Utilities/Queue.swift index 0103df5acc..d0ffebe4db 100644 --- a/DatadogSessionReplay/Sources/Utilities/Queue.swift +++ b/DatadogSessionReplay/Sources/Utilities/Queue.swift @@ -7,11 +7,11 @@ #if os(iOS) import Foundation -internal protocol Queue { +internal protocol Queue: AnyObject { func run(_ block: @escaping () -> Void) } -internal struct MainAsyncQueue: Queue { +internal class MainAsyncQueue: Queue { private let queue: DispatchQueue = .main func run(_ block: @escaping () -> Void) { @@ -19,7 +19,7 @@ internal struct MainAsyncQueue: Queue { } } -internal struct BackgroundAsyncQueue: Queue { +internal class BackgroundAsyncQueue: Queue { private let queue: DispatchQueue init(named queueName: String) { From 06b4dc73f391e2157585dcd68f768d56a7fc7e34 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 9 Jan 2024 12:12:47 +0000 Subject: [PATCH 15/60] RUM-2153 Fix lint --- .../NodeRecorders/UIImageViewRecorder.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 0d487b7fd8..8329de7d66 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -7,11 +7,11 @@ #if os(iOS) import UIKit -struct UIImageResource { - let image: UIImage - let tintColor: UIColor? +internal struct UIImageResource { + internal let image: UIImage + internal let tintColor: UIColor? - init(image: UIImage, tintColor: UIColor?) { + internal init(image: UIImage, tintColor: UIColor?) { self.image = image self.tintColor = tintColor } From 92db7739924db5d2077b3a99ef329470688fbb37 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 9 Jan 2024 13:22:10 +0000 Subject: [PATCH 16/60] RUM-2153 Fix tests after refactor --- DatadogSessionReplay/Tests/Mocks/QueueMocks.swift | 2 +- DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift | 10 +++++++++- .../Tests/Processor/ResourceProcessorTests.swift | 12 ++++++++++-- .../ViewTreeSnapshot/ViewTreeRecorderTests.swift | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift b/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift index 3447e6ab4d..7b399f333b 100644 --- a/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/QueueMocks.swift @@ -8,7 +8,7 @@ import Foundation @testable import DatadogSessionReplay /// A queue that executes synchronously on the caller thread. -internal struct NoQueue: Queue { +internal class NoQueue: Queue { func run(_ block: @escaping () -> Void) { block() } diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index 56eae2605c..6699a4b168 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -267,13 +267,21 @@ extension Node: AnyMockable, RandomMockable { struct MockResource: Resource, AnyMockable, RandomMockable { var identifier: String - let data: Data + var data: Data init(identifier: String, data: Data) { self.identifier = identifier self.data = data } + func calculateIdentifier() -> String { + return identifier + } + + func calculateData() -> Data { + return data + } + static func mockAny() -> MockResource { return MockResource(identifier: .mockAny(), data: .mockAny()) } diff --git a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift index 73c683088d..60bc728849 100644 --- a/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/ResourceProcessorTests.swift @@ -37,8 +37,16 @@ class ResourceProcessorTests: XCTestCase { XCTAssertEqual( writer.resources[0], [ - EnrichedResource(resource: resource1, context: context), - EnrichedResource(resource: resource2, context: context) + EnrichedResource( + identifier: resource1.calculateIdentifier(), + data: resource1.calculateData(), + context: context + ), + EnrichedResource( + identifier: resource2.calculateIdentifier(), + data: resource2.calculateData(), + context: context + ), ] ) } diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index 18fba2d362..161a6d1633 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -144,7 +144,7 @@ class ViewTreeRecorderTests: XCTestCase { XCTAssertEqual(expectedNodes, actualNodes, "Nodes must be recorded in DFS order") let expectedResources = ["rootResource", "a", "aa", "aav1", "aav2", "aav3", "ab", "aba", "abb", "b", "c"] - let actualResources = resources.map { $0.identifier } + let actualResources = resources.map { $0.calculateIdentifier() } XCTAssertEqual(expectedResources, actualResources, "Resources must be recorded in DFS order") let expectedQueriedViews: [UIView] = [rootView, a, b, c, aa, ab, aba, abb] From 035cf2f5a58306d621abe4bc4a07ebbe9b3843c4 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 9 Jan 2024 14:46:55 +0000 Subject: [PATCH 17/60] RUM-2153 Fix snapshot tests --- .../SRSnapshotTests/Utils/SnapshotTestCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift index 634457e604..feb592e706 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift @@ -114,6 +114,6 @@ internal class SnapshotTestCase: XCTestCase { // MARK: - SR Mocks -private struct NoQueue: Queue { +private class NoQueue: Queue { func run(_ block: @escaping () -> Void) { block() } } From 89c62e4c0c92dd2bc581a73baa3dfa9ba837be39 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 9 Jan 2024 14:54:50 +0000 Subject: [PATCH 18/60] Do not start SR when it is sampled out --- DatadogSessionReplay/Sources/SessionReplay.swift | 3 +++ DatadogSessionReplay/Tests/SessionReplayTests.swift | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/DatadogSessionReplay/Sources/SessionReplay.swift b/DatadogSessionReplay/Sources/SessionReplay.swift index 80272192e4..8eab5c2189 100644 --- a/DatadogSessionReplay/Sources/SessionReplay.swift +++ b/DatadogSessionReplay/Sources/SessionReplay.swift @@ -37,6 +37,9 @@ public enum SessionReplay { description: "Datadog SDK must be initialized before calling `SessionReplay.enable(with:)`." ) } + guard configuration.replaySampleRate > 0 else { + return + } let sessionReplay = try SessionReplayFeature(core: core, configuration: configuration) try core.register(feature: sessionReplay) diff --git a/DatadogSessionReplay/Tests/SessionReplayTests.swift b/DatadogSessionReplay/Tests/SessionReplayTests.swift index b94c948d00..c58c976226 100644 --- a/DatadogSessionReplay/Tests/SessionReplayTests.swift +++ b/DatadogSessionReplay/Tests/SessionReplayTests.swift @@ -128,4 +128,16 @@ class SessionReplayTests: XCTestCase { let sr = try XCTUnwrap(core.get(feature: SessionReplayFeature.self)) XCTAssertEqual(sr.recordingCoordinator.sampler.samplingRate, random) } + + func testItDoesntStartFeatureWhenSamplingRateIsZero() throws { + // Given + config.replaySampleRate = 0 + + // When + SessionReplay.enable(with: config, in: core) + + // Then + XCTAssertNil(core.get(feature: SessionReplayFeature.self)) + XCTAssertNil(core.get(feature: ResourcesFeature.self)) + } } From 75cf045b00df4f52bc51cd8147343c41997daf5a Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 10 Jan 2024 15:23:40 +0000 Subject: [PATCH 19/60] RUM-2153 Add threading documentation --- .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 78f0c6f444..abf5c5fd30 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -57,8 +57,10 @@ internal typealias Node = SessionReplayNode @_spi(Internal) public protocol SessionReplayResource { /// Calculates the unique identifier of the resource. + /// This function is not thread safe and needs to be synchronized by the caller. func calculateIdentifier() -> String /// Calculates the data of the resource. + /// This function is not thread safe and needs to be synchronized by the caller. func calculateData() -> Data } From 947f8b8c2f1a041311e7f0ade190171fdff65796 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 5 Jan 2024 10:48:36 +0100 Subject: [PATCH 20/60] RUM-699 fix: RUM context not being linked to started span on the same thread --- CHANGELOG.md | 4 + Datadog/Datadog.xcodeproj/project.pbxproj | 46 ++---- DatadogCore/Sources/Core/DatadogCore.swift | 32 ++-- .../DatadogCore/DatadogCoreTests.swift | 50 ++++++ .../DatadogInternal/DatadogCoreProxy.swift | 18 +- .../Tests/Datadog/Mocks/LogsMocks.swift | 21 --- .../Datadog/Mocks/TracingFeatureMocks.swift | 64 +++++-- DatadogCore/Tests/Datadog/TracerTests.swift | 143 +++++++++------- .../TracingURLSessionHandlerTests.swift | 2 +- .../Sources/DatadogCoreProtocol.swift | 39 +++-- .../Sources/LogOutputs/LogFileOutput.swift | 17 -- .../Sources/LogOutputs/LogOutput.swift | 12 -- .../Tests/LogOutputs/LogFileOutputTests.swift | 26 --- .../Tests/Mocks/LoggingFeatureMocks.swift | 21 --- DatadogTrace/Sources/DDSpan.swift | 156 +++++++----------- DatadogTrace/Sources/DDSpanContext.swift | 2 +- DatadogTrace/Sources/DatadogTracer.swift | 44 ++--- DatadogTrace/Sources/Feature/Baggages.swift | 2 +- .../Sources/Feature/MessageReceivers.swift | 29 ---- .../Sources/Feature/TraceFeature.swift | 21 +-- .../Sources/OpenTracing/OTConstants.swift | 1 - .../Sources/Span/SpanEventBuilder.swift | 20 ++- .../Sources/Span/SpanWriteContext.swift | 51 ++++++ DatadogTrace/Sources/Trace.swift | 2 +- DatadogTrace/Sources/Tracer.swift | 19 ++- .../Tests/ContextMessageReceiverTests.swift | 137 +-------------- DatadogTrace/Tests/DDSpanTests.swift | 19 +-- .../Tests/Span/SpanEventBuilderTests.swift | 101 +++++++++++- .../Tests/Span/SpanWriteContextTests.swift | 51 ++++++ DatadogTrace/Tests/TraceTests.swift | 16 +- DatadogTrace/Tests/TracingFeatureMocks.swift | 40 +++-- .../Tests/TracingURLSessionHandlerTests.swift | 8 +- .../Tests/Utils/ActiveSpansPoolTests.swift | 22 ++- TestUtilities/Helpers/XCTestCase.swift | 9 + .../Mocks/CoreMocks/PassthroughCoreMock.swift | 12 +- 35 files changed, 662 insertions(+), 595 deletions(-) delete mode 100644 DatadogLogs/Sources/LogOutputs/LogFileOutput.swift delete mode 100644 DatadogLogs/Sources/LogOutputs/LogOutput.swift delete mode 100644 DatadogLogs/Tests/LogOutputs/LogFileOutputTests.swift create mode 100644 DatadogTrace/Sources/Span/SpanWriteContext.swift create mode 100644 DatadogTrace/Tests/Span/SpanWriteContextTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa0f77b4d..603a0536d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - [FEATURE] Add `BatchProcessingLevel` configuration allowing to process more batches within single read/upload cycle. See [#1531][] - [FIX] Use `currentRequest` instead `originalRequest` for URLSession request interception - [FIX] Remove weak `UIViewController` references. See [#1597][] +- [FIX] Use `currentRequest` instead `originalRequest` for URLSession request interception. See [#1609][] +- [FIX] RUM session not being linked to spans. See [#1615][] # 2.5.1 / 20-12-2023 @@ -570,6 +572,8 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1533]: https://github.com/DataDog/dd-sdk-ios/pull/1533 [#1594]: https://github.com/DataDog/dd-sdk-ios/pull/1594 [#1536]: https://github.com/DataDog/dd-sdk-ios/pull/1536 +[#1609]: [https://github.com/DataDog/dd-sdk-ios/pull/1609] +[#1615]: [https://github.com/DataDog/dd-sdk-ios/pull/1615] [#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 [#1596]: https://github.com/DataDog/dd-sdk-ios/pull/1596 [#1597]: https://github.com/DataDog/dd-sdk-ios/pull/1597 diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index b44b0f4887..cabf1e58c3 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -333,6 +333,8 @@ 6188697D2A4376F700E8996B /* RUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */; }; 6188900F2AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */; }; 618890102AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */; }; + 618C0FC02B482F6800266B38 /* SpanWriteContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */; }; + 618C0FC12B482F6800266B38 /* SpanWriteContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */; }; 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C365E248E85B400520CDE /* DateFormattingTests.swift */; }; 618F9843265BC486009959F8 /* E2EInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F9842265BC486009959F8 /* E2EInstrumentationTests.swift */; }; 618F984E265BC905009959F8 /* E2EConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F984D265BC905009959F8 /* E2EConfig.swift */; }; @@ -416,6 +418,8 @@ 61C713D12A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */; }; 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; + 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; + 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; 61D3E0D2277B23F1008BE766 /* KronosInternetAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */; }; 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */; }; @@ -532,15 +536,11 @@ D20731AB29A5279D00ECBF94 /* LogsFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F1FAF283E227100651A3A /* LogsFeature.swift */; }; D20731B529A528DA00ECBF94 /* LogEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC32423979B00786299 /* LogEventBuilder.swift */; }; D20731B629A528DA00ECBF94 /* LogEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC32423979B00786299 /* LogEventBuilder.swift */; }; - D20731C129A528EB00ECBF94 /* LogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC82423979B00786299 /* LogOutput.swift */; }; D20731C229A528EB00ECBF94 /* LogEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC22423979B00786299 /* LogEventEncoder.swift */; }; D20731C329A528EB00ECBF94 /* LogEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C1F5B271484B400922024 /* LogEventMapper.swift */; }; - D20731C429A528EC00ECBF94 /* LogFileOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC72423979B00786299 /* LogFileOutput.swift */; }; D20731C529A528EC00ECBF94 /* LogEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC42423979B00786299 /* LogEventSanitizer.swift */; }; - D20731C629A528ED00ECBF94 /* LogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC82423979B00786299 /* LogOutput.swift */; }; D20731C729A528ED00ECBF94 /* LogEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC22423979B00786299 /* LogEventEncoder.swift */; }; D20731C829A528ED00ECBF94 /* LogEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C1F5B271484B400922024 /* LogEventMapper.swift */; }; - D20731C929A528ED00ECBF94 /* LogFileOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC72423979B00786299 /* LogFileOutput.swift */; }; D20731CA29A528ED00ECBF94 /* LogEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC42423979B00786299 /* LogEventSanitizer.swift */; }; D20731CB29A52E6000ECBF94 /* Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B8F2768FDDE00870CBF /* Sampler.swift */; }; D20731CC29A52E6000ECBF94 /* Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B8F2768FDDE00870CBF /* Sampler.swift */; }; @@ -1066,7 +1066,6 @@ D2A783DA29A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; D2A783E729A53468003B03BB /* LogEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */; }; D2A783E829A53468003B03BB /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194D51B287ECDC00091547D /* ConsoleLoggerTests.swift */; }; - D2A783E929A53468003B03BB /* LogFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C402423990D00786299 /* LogFileOutputTests.swift */; }; D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */; }; D2A783EB29A53468003B03BB /* LogSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3C2423990D00786299 /* LogSanitizerTests.swift */; }; D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; @@ -1075,7 +1074,6 @@ D2A783F529A534F9003B03BB /* LogEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */; }; D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */; }; - D2A783F829A534F9003B03BB /* LogFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C402423990D00786299 /* LogFileOutputTests.swift */; }; D2A783FB29A534F9003B03BB /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; D2A7840329A536AD003B03BB /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */; }; D2A7840429A536AD003B03BB /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */; }; @@ -2048,8 +2046,6 @@ 61133BC22423979B00786299 /* LogEventEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventEncoder.swift; sourceTree = ""; }; 61133BC32423979B00786299 /* LogEventBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventBuilder.swift; sourceTree = ""; }; 61133BC42423979B00786299 /* LogEventSanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventSanitizer.swift; sourceTree = ""; }; - 61133BC72423979B00786299 /* LogFileOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogFileOutput.swift; sourceTree = ""; }; - 61133BC82423979B00786299 /* LogOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogOutput.swift; sourceTree = ""; }; 61133BF0242397DA00786299 /* DatadogObjc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogObjc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61133BF2242397DA00786299 /* DatadogObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DatadogObjc.h; sourceTree = ""; }; 61133BF3242397DA00786299 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2076,7 +2072,6 @@ 61133C382423990D00786299 /* LoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventBuilderTests.swift; sourceTree = ""; }; 61133C3C2423990D00786299 /* LogSanitizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogSanitizerTests.swift; sourceTree = ""; }; - 61133C402423990D00786299 /* LogFileOutputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogFileOutputTests.swift; sourceTree = ""; }; 61133C412423990D00786299 /* DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogTests.swift; sourceTree = ""; }; 61133C432423990D00786299 /* LogMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogMatcher.swift; sourceTree = ""; }; 61133C462423990D00786299 /* TestsDirectory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestsDirectory.swift; sourceTree = ""; }; @@ -2220,6 +2215,7 @@ 6187A53826FCBE240015D94A /* TracerE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerE2ETests.swift; sourceTree = ""; }; 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMConfigurationTests.swift; sourceTree = ""; }; 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverMock.swift; sourceTree = ""; }; + 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanWriteContextTests.swift; sourceTree = ""; }; 618C365E248E85B400520CDE /* DateFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormattingTests.swift; sourceTree = ""; }; 618D9DE6263AD78900A3FAD2 /* SpanEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanEventMapper.swift; sourceTree = ""; }; 618DCFD624C7265300589570 /* RUMUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUUID.swift; sourceTree = ""; }; @@ -2318,6 +2314,7 @@ 61C713C92A3DC22700FA735A /* RUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTests.swift; sourceTree = ""; }; 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRegistrationCoreMock.swift; sourceTree = ""; }; 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyHelpers.swift; sourceTree = ""; }; + 61CE58592B48174D00479510 /* SpanWriteContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanWriteContext.swift; sourceTree = ""; }; 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objcTests.swift"; sourceTree = ""; }; 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosInternetAddress.swift; sourceTree = ""; }; 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosDNSResolver.swift; sourceTree = ""; }; @@ -3812,15 +3809,6 @@ path = Log; sourceTree = ""; }; - 61133BC52423979B00786299 /* LogOutputs */ = { - isa = PBXGroup; - children = ( - 61133BC72423979B00786299 /* LogFileOutput.swift */, - 61133BC82423979B00786299 /* LogOutput.swift */, - ); - path = LogOutputs; - sourceTree = ""; - }; 61133BF1242397DA00786299 /* DatadogObjc */ = { isa = PBXGroup; children = ( @@ -4026,14 +4014,6 @@ path = Log; sourceTree = ""; }; - 61133C3D2423990D00786299 /* LogOutputs */ = { - isa = PBXGroup; - children = ( - 61133C402423990D00786299 /* LogFileOutputTests.swift */, - ); - path = LogOutputs; - sourceTree = ""; - }; 61133C422423990D00786299 /* Matchers */ = { isa = PBXGroup; children = ( @@ -4778,6 +4758,7 @@ 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */, 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */, 614872762485067300E3EBDB /* SpanTagsReducer.swift */, + 61CE58592B48174D00479510 /* SpanWriteContext.swift */, ); path = Span; sourceTree = ""; @@ -4848,6 +4829,7 @@ children = ( 61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */, 61122EE725B1C92500F9C7F5 /* SpanSanitizerTests.swift */, + 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */, ); path = Span; sourceTree = ""; @@ -5161,7 +5143,6 @@ 6194E4BB2878AF7600EB6307 /* ConsoleLogger.swift */, D24C9C4129A7986E002057CF /* Feature */, 61133BC12423979B00786299 /* Log */, - 61133BC52423979B00786299 /* LogOutputs */, D22C1F5A2714849700922024 /* Scrubbing */, ); name = DatadogLogs; @@ -5178,7 +5159,6 @@ D242C2A02A14D747004B4980 /* RemoteLoggerTests.swift */, D20FD9CE2AC6FF42004D3569 /* WebViewLogReceiverTests.swift */, 61133C3A2423990D00786299 /* Log */, - 61133C3D2423990D00786299 /* LogOutputs */, D2A783EC29A534DB003B03BB /* Mocks */, ); name = DatadogLogsTests; @@ -7929,11 +7909,9 @@ D207319629A522F600ECBF94 /* ConsoleLogger.swift in Sources */, D20731C529A528EC00ECBF94 /* LogEventSanitizer.swift in Sources */, 49D8C0BD2AC5F2BB0075E427 /* Logs+Internal.swift in Sources */, - D20731C429A528EC00ECBF94 /* LogFileOutput.swift in Sources */, D207319529A522F600ECBF94 /* LogsFeature.swift in Sources */, D242C29E2A14D6A6004B4980 /* RemoteLogger.swift in Sources */, D20731B529A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, - D20731C129A528EB00ECBF94 /* LogOutput.swift in Sources */, D243BBF529A620CC000B9CEC /* MessageReceivers.swift in Sources */, D22C5BC92A98A0B30024CC1F /* Baggages.swift in Sources */, ); @@ -7952,7 +7930,6 @@ D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249972A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */, - D2A783E929A53468003B03BB /* LogFileOutputTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7970,11 +7947,9 @@ D20731A929A5279D00ECBF94 /* ConsoleLogger.swift in Sources */, D20731CA29A528ED00ECBF94 /* LogEventSanitizer.swift in Sources */, 49D8C0BE2AC5F2BC0075E427 /* Logs+Internal.swift in Sources */, - D20731C929A528ED00ECBF94 /* LogFileOutput.swift in Sources */, D20731AB29A5279D00ECBF94 /* LogsFeature.swift in Sources */, D242C29F2A14D6A7004B4980 /* RemoteLogger.swift in Sources */, D20731B629A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, - D20731C629A528ED00ECBF94 /* LogOutput.swift in Sources */, D243BBF629A620CC000B9CEC /* MessageReceivers.swift in Sources */, D22C5BC82A98A0B20024CC1F /* Baggages.swift in Sources */, ); @@ -8277,6 +8252,7 @@ D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */, D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */, D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */, + 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */, D2C1A51A29C4C5DD00946C31 /* JSONEncoder.swift in Sources */, D2C1A51829C4C53F00946C31 /* OTSpan.swift in Sources */, D2C1A51429C4C53F00946C31 /* OTSpanContext.swift in Sources */, @@ -8314,6 +8290,7 @@ 619CE75E2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A52329C4C75700946C31 /* WarningsTests.swift in Sources */, D2C1A51D29C4C75700946C31 /* SpanEventBuilderTests.swift in Sources */, + 618C0FC02B482F6800266B38 /* SpanWriteContextTests.swift in Sources */, D2C1A52229C4C75700946C31 /* DDNoopTracerTests.swift in Sources */, D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, @@ -8453,7 +8430,6 @@ D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249982A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */, - D2A783F829A534F9003B03BB /* LogFileOutputTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8466,6 +8442,7 @@ D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */, D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */, D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */, + 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */, D2C1A53C29C4F2DF00946C31 /* JSONEncoder.swift in Sources */, D2C1A53D29C4F2DF00946C31 /* OTSpan.swift in Sources */, D2C1A53E29C4F2DF00946C31 /* OTSpanContext.swift in Sources */, @@ -8503,6 +8480,7 @@ 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, D2C1A56129C4F2E800946C31 /* WarningsTests.swift in Sources */, D2C1A56229C4F2E800946C31 /* SpanEventBuilderTests.swift in Sources */, + 618C0FC12B482F6800266B38 /* SpanWriteContextTests.swift in Sources */, D2C1A56329C4F2E800946C31 /* DDNoopTracerTests.swift in Sources */, D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index aa8464b22a..6d0bca0804 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -292,8 +292,7 @@ extension DatadogCore: DatadogCoreProtocol { return DatadogCoreFeatureScope( contextProvider: contextProvider, - storage: storage, - telemetry: telemetry + storage: storage ) } @@ -309,24 +308,21 @@ extension DatadogCore: DatadogCoreProtocol { internal struct DatadogCoreFeatureScope: FeatureScope { let contextProvider: DatadogContextProvider let storage: FeatureStorage - let telemetry: Telemetry - func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) throws -> Void) { - // On user thread: request SDK context. - contextProvider.read { context in - // On context thread: request writer for current tracking consent. - let writer = storage.writer( - for: bypassConsent ? .granted : context.trackingConsent, - forceNewBatch: forceNewBatch - ) + func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { + // (on user thread) request SDK context + context { context in + // (on context thread) call the block + let writer = storage.writer(for: bypassConsent ? .granted : context.trackingConsent, forceNewBatch: forceNewBatch) + block(context, writer) + } + } - // Still on context thread: send `Writer` to EWC caller. The writer implements `AsyncWriter`, so - // the implementation of `writer.write(value:)` will run asynchronously without blocking the context thread. - do { - try block(context, writer) - } catch { - telemetry.error("Failed to execute feature scope", error: error) - } + func context(_ block: @escaping (DatadogContext) -> Void) { + // (on user thread) request SDK context + contextProvider.read { context in + // (on context thread) call the block + block(context) } } } diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift index e0865648c7..081d4dd2d1 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -186,6 +186,56 @@ class DatadogCoreTests: XCTestCase { XCTAssertEqual(requestBuilderSpy.requestParameters.count, 3, "It should send 3 requests") } + func testWhenFeatureBaggageIsUpdated_thenNewValueIsImmediatellyAvailable() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + let feature = FeatureMock() + try core.register(feature: feature) + let scope = try XCTUnwrap(core.scope(for: FeatureMock.name)) + + // When + let key = "key" + let expectation1 = self.expectation(description: "retrieve context") + let expectation2 = self.expectation(description: "retrieve context and event writer") + expectation1.expectedFulfillmentCount = 2 + expectation2.expectedFulfillmentCount = 2 + + core.set(baggage: "baggage 1", forKey: key) + scope.context { context in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 1") + expectation1.fulfill() + } + scope.eventWriteContext { context, _ in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 1") + expectation2.fulfill() + } + + core.set(baggage: "baggage 2", forKey: key) + scope.context { context in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 2") + expectation1.fulfill() + } + scope.eventWriteContext { context, _ in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 2") + expectation2.fulfill() + } + + waitForExpectations(timeout: 1) + } + func testWhenPerformancePresetOverrideIsProvided_itOverridesPresets() throws { // Given let core1 = DatadogCore( diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index f2b3f61979..542f11b838 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -110,10 +110,18 @@ private struct FeatureScopeProxy: FeatureScope { let proxy: FeatureScope let interceptor: FeatureScopeInterceptor - func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) throws -> Void) { + func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { interceptor.enter() proxy.eventWriteContext(bypassConsent: bypassConsent, forceNewBatch: forceNewBatch) { context, writer in - try block(context, interceptor.intercept(writer: writer)) + block(context, interceptor.intercept(writer: writer)) + interceptor.leave() + } + } + + func context(_ block: @escaping (DatadogContext) -> Void) { + interceptor.enter() + proxy.context { context in + block(context) interceptor.leave() } } @@ -123,10 +131,14 @@ private class FeatureScopeInterceptor { struct InterceptingWriter: Writer { static let jsonEncoder = JSONEncoder.dd.default() + let group: DispatchGroup let actualWriter: Writer unowned var interception: FeatureScopeInterceptor? func write(value: T, metadata: M) { + group.enter() + defer { group.leave() } + actualWriter.write(value: value, metadata: metadata) let event = value @@ -136,7 +148,7 @@ private class FeatureScopeInterceptor { } func intercept(writer: Writer) -> Writer { - return InterceptingWriter(actualWriter: writer, interception: self) + return InterceptingWriter(group: group, actualWriter: writer, interception: self) } // MARK: - Synchronizing and awaiting events: diff --git a/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift index 2d66d1b38f..500d0ef8fd 100644 --- a/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift @@ -304,24 +304,3 @@ extension LogEvent.Attributes: Equatable { && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) } } - -/// `LogOutput` recording received logs. -class LogOutputMock: LogOutput { - var onLogRecorded: ((LogEvent) -> Void)? - - var recordedLog: LogEvent? - var allRecordedLogs: [LogEvent] = [] - - func write(log: LogEvent) { - recordedLog = log - allRecordedLogs.append(log) - onLogRecorded?(log) - } - - /// Returns newline-separated `String` description of all recorded logs. - func dumpAllRecordedLogs() -> String { - return allRecordedLogs - .map { "- \($0)" } - .joined(separator: "\n") - } -} diff --git a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift index a61a3c9e90..87a3feef3e 100644 --- a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift @@ -15,10 +15,22 @@ extension DatadogCoreProxy { return try waitAndReturnEventsData(ofFeature: TraceFeature.name) .map { eventData in try SpanMatcher.fromJSONObjectData(eventData) } } + + func waitAndReturnSpanEvents(file: StaticString = #file, line: UInt = #line) -> [SpanEvent] { + return waitAndReturnEvents(ofFeature: TraceFeature.name, ofType: SpanEventsEnvelope.self) + .map { envelope in + precondition(envelope.spans.count == 1, "Only expect one `SpanEvent` per envelope") + return envelope.spans[0] + } + } } // MARK: - Span Mocks +internal struct NOPSpanWriteContext: SpanWriteContext { + func spanWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) {} +} + extension DDSpan { static func mockAny(in core: DatadogCoreProtocol) -> DDSpan { return mockWith(core: core) @@ -29,14 +41,18 @@ extension DDSpan { context: DDSpanContext = .mockAny(), operationName: String = .mockAny(), startTime: Date = .mockAny(), - tags: [String: Encodable] = [:] + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() ) -> DDSpan { return DDSpan( tracer: tracer, context: context, operationName: operationName, startTime: startTime, - tags: tags + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter ) } @@ -45,14 +61,18 @@ extension DDSpan { context: DDSpanContext = .mockAny(), operationName: String = .mockAny(), startTime: Date = .mockAny(), - tags: [String: Encodable] = [:] + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() ) -> DDSpan { return DDSpan( tracer: .mockAny(in: core), context: context, operationName: operationName, startTime: startTime, - tags: tags + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter ) } } @@ -94,25 +114,19 @@ extension DatadogTracer { core: DatadogCoreProtocol, sampler: Sampler = .mockKeepAll(), tags: [String: Encodable] = [:], - service: String? = nil, - networkInfoEnabled: Bool = true, - spanEventMapper: ((SpanEvent) -> SpanEvent)? = nil, tracingUUIDGenerator: TraceIDGenerator = DefaultTraceIDGenerator(), dateProvider: DateProvider = SystemDateProvider(), - contextReceiver: ContextMessageReceiver = .mockAny(), + spanEventBuilder: SpanEventBuilder = .mockAny(), loggingIntegration: TracingWithLoggingIntegration = .mockAny() ) -> DatadogTracer { return DatadogTracer( core: core, sampler: sampler, tags: tags, - service: service, - networkInfoEnabled: networkInfoEnabled, - spanEventMapper: spanEventMapper, tracingUUIDGenerator: tracingUUIDGenerator, dateProvider: dateProvider, - contextReceiver: contextReceiver, - loggingIntegration: loggingIntegration + loggingIntegration: loggingIntegration, + spanEventBuilder: spanEventBuilder ) } } @@ -129,6 +143,28 @@ extension TracingWithLoggingIntegration { extension ContextMessageReceiver { static func mockAny() -> ContextMessageReceiver { - return ContextMessageReceiver(bundleWithRumEnabled: true) + return ContextMessageReceiver() + } +} + +extension SpanEventBuilder { + static func mockAny() -> SpanEventBuilder { + return mockWith() + } + + static func mockWith( + service: String = .mockAny(), + networkInfoEnabled: Bool = false, + eventsMapper: SpanEventMapper? = nil, + bundleWithRUM: Bool = false, + telemetry: Telemetry = NOPTelemetry() + ) -> SpanEventBuilder { + return SpanEventBuilder( + service: service, + networkInfoEnabled: networkInfoEnabled, + eventsMapper: eventsMapper, + bundleWithRUM: bundleWithRUM, + telemetry: telemetry + ) } } diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index 1b8815bab7..0d265975c2 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -11,6 +11,7 @@ import DatadogInternal @testable import DatadogTrace @testable import DatadogLogs @testable import DatadogCore +@testable import DatadogRUM // swiftlint:disable multiline_arguments_brackets class TracerTests: XCTestCase { @@ -340,11 +341,9 @@ class TracerTests: XCTestCase { let tracer = Tracer.shared(in: core).dd tracer.startSpan(operationName: "span with no user info").finish() - tracer.queue.sync {} // wait for processing the span event in `DDSpan` core.context.userInfo = UserInfo(id: "abc-123", name: "Foo", email: nil, extraInfo: [:]) tracer.startSpan(operationName: "span with user `id` and `name`").finish() - tracer.queue.sync {} core.context.userInfo = UserInfo( id: "abc-123", @@ -357,7 +356,6 @@ class TracerTests: XCTestCase { ] ) tracer.startSpan(operationName: "span with user `id`, `name`, `email` and `extraInfo`").finish() - tracer.queue.sync {} core.context.userInfo = .empty tracer.startSpan(operationName: "span with no user info").finish() @@ -403,7 +401,6 @@ class TracerTests: XCTestCase { ) tracer.startSpan(operationName: "span with carrier info").finish() - tracer.queue.sync {} // wait for processing the span event in `DDSpan` // simulate leaving cellular service range core.context.carrierInfo = nil @@ -442,7 +439,6 @@ class TracerTests: XCTestCase { ) tracer.startSpan(operationName: "online span").finish() - tracer.queue.sync {} // wait for processing the span event in `DDSpan` // simulate unreachable network core.context.networkConnectionInfo = .mockWith( @@ -658,57 +654,92 @@ class TracerTests: XCTestCase { } // MARK: - Integration With RUM Feature -// // TODO: RUMM-2843 [V2 regression] RUM context is not associated with span started on caller thread -// func testGivenBundlingWithRUMEnabledAndRUMMonitorRegistered_whenSendingSpanBeforeAnyUserActivity_itContainsSessionId() throws { -// let tracing: TracingFeature = .mockAny() -// core.register(feature: tracing) -// -// let rum: RUMFeature = .mockAny() -// core.register(feature: rum) -// -// // given -// Global.sharedTracer = Tracer.initialize(configuration: .init(), in: core).dd -// defer { Global.sharedTracer = DDNoopTracer() } -// Global.rum = RUMMonitor.initialize(in: core) -// defer { Global.rum = DDNoopRUMMonitor() } -// -// // when -// let span = Global.sharedTracer.startSpan(operationName: "operation", tags: [:], startTime: Date()) -// span.finish() -// -// // then -// let spanMatcher = try core.waitAndReturnSpanMatchers()[0] -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta.\(RUMContextAttributes.IDs.sessionID)")) -// } - - // TODO: RUMM-2843 [V2 regression] RUM context is not associated with span started on caller thread -// func testGivenBundlingWithRUMEnabledAndRUMMonitorRegistered_whenSendingSpan_itContainsCurrentRUMContext() throws { -// let tracing: TracingFeature = .mockAny() -// core.register(feature: tracing) -// -// let rum: RUMFeature = .mockAny() -// core.register(feature: rum) -// -// // given -// Global.sharedTracer = DatadogTracer.initialize(in: core).dd -// defer { Global.sharedTracer = DDNoopTracer() } -// Global.rum = RUMMonitor.initialize(in: core) -// Global.rum.startView(viewController: mockView) -// defer { Global.rum = DDNoopRUMMonitor() } -// -// // when -// let span = Global.sharedTracer.startSpan(operationName: "operation", tags: [:], startTime: Date()) -// span.finish() -// -// // then -// let spanMatcher = try core.waitAndReturnSpanMatchers()[0] -// XCTAssertEqual( -// try spanMatcher.meta.custom(keyPath: "meta._dd.application.id"), -// rum.configuration.applicationID -// ) -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta._dd.session.id")) -// XCTAssertValidRumUUID(try spanMatcher.meta.custom(keyPath: "meta._dd.view.id")) -// } + + func testGivenBundleWithRumEnabled_whenSendingSpanBeforeAnyInteraction_itContainsViewId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], "rum-app-id") + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumViewID], rumEvent.view.id) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } + + func testGivenBundleWithRumEnabled_whenStartingSpanWhileUserInteractionIsPending_itContainsActionId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + + // When + RUMMonitor.shared(in: core).startAction(type: .swipe, name: "swipe") + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + RUMMonitor.shared(in: core).stopAction(type: .swipe, name: "swipe") + span.finish() + + // Then + let rumEvent = try XCTUnwrap( + core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMActionEvent.self).first(where: { $0.action.type == .swipe }) + ) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], rumEvent.application.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumViewID], rumEvent.view.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumActionID], rumEvent.action.id) + } + + func testGivenBundleWithRumEnabled_whenSendingSpanAfterViewIsStopped_itContainsSessionId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + RUMMonitor.shared(in: core).startView(key: "view", name: "view") + + // When + RUMMonitor.shared(in: core).stopView(key: "view") + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], rumEvent.application.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertNil(spanEvent.tags[SpanTags.rumViewID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } + + func testGivenBundleWithRumDisabled_whenSendingSpan_itDoesNotContainRUMContext() throws { + config.bundleWithRumEnabled = false + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + RUMMonitor.shared(in: core).startView(key: "view", name: "view") + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertNil(spanEvent.tags[SpanTags.rumApplicationID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumSessionID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumViewID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } // MARK: - Injecting span context into carrier diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index 10f5e59a22..1f9ffa7f2e 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -20,7 +20,7 @@ class TracingURLSessionHandlerTests: XCTestCase { override func setUp() { super.setUp() - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) + let receiver = ContextMessageReceiver() core = PassthroughCoreMock(messageReceiver: CombinedFeatureMessageReceiver([ LogMessageReceiver.mockAny(), receiver diff --git a/DatadogInternal/Sources/DatadogCoreProtocol.swift b/DatadogInternal/Sources/DatadogCoreProtocol.swift index 44864f94b2..0e2f8cebc8 100644 --- a/DatadogInternal/Sources/DatadogCoreProtocol.swift +++ b/DatadogInternal/Sources/DatadogCoreProtocol.swift @@ -201,14 +201,14 @@ extension DatadogCoreProtocol { /// Feature scope provides a context and a writer to build a record event. public protocol FeatureScope { - /// Retrieve the event context and writer. + /// Retrieve the core context and event writer. /// - /// The Feature scope provides the current Datadog context and event writer - /// for the Feature to build and record events. + /// The Feature scope provides the current Datadog context and event writer for building and recording events. + /// The provided context is valid at the moment of the call, meaning that it includes all changes that happened + /// earlier on the same thread. /// - /// A Feature has the ability to bypass the current user consent for data collection. The `bypassConsent` - /// must be set to `true` only if the Feature is already aware of the user's consent for the event it is about - /// to write. + /// A Feature has the ability to bypass the current user consent for data collection. Set `bypassConsent` to `true` + /// only if the Feature is already aware of the user's consent for the event it is about to write. /// /// - Parameters: /// - bypassConsent: `true` to bypass the current core consent and write events as authorized. @@ -218,16 +218,27 @@ public protocol FeatureScope { /// Default is `false`, which means the core uses its own heuristic to split events between /// batches. This parameter can be leveraged in Features which require a clear separation /// of group of events for preparing their upload (a single upload is always constructed from a single batch). - /// - block: The block to execute. - func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) throws -> Void) + /// - block: The block to execute; it is called on the context queue. + func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) + + /// Retrieve the core context. + /// + /// A feature can use this method to request the Datadog context valid at the moment of the call. + /// + /// - Parameter block: The block to execute; it is called on the context queue. + func context(_ block: @escaping (DatadogContext) -> Void) } /// Feature scope provides a context and a writer to build a record event. public extension FeatureScope { - /// Retrieve the event context and writer. + /// Retrieve the core context and event writer. + /// + /// The Feature scope provides the current Datadog context and event writer for building and recording events. + /// The provided context is valid at the moment of the call, meaning that it includes all changes that happened + /// earlier on the same thread. /// - /// The Feature scope provides the current Datadog context and event writer - /// for the Feature to build and record events. + /// A Feature has the ability to bypass the current user consent for data collection. Set `bypassConsent` to `true` + /// only if the Feature is already aware of the user's consent for the event it is about to write. /// /// - Parameters: /// - bypassConsent: `true` to bypass the current core consent and write events as authorized. @@ -237,9 +248,9 @@ public extension FeatureScope { /// Default is `false`, which means the core uses its own heuristic to split events between /// batches. This parameter can be leveraged in Features which require a clear separation /// of group of events for preparing their upload (a single upload is always constructed from a single batch). - /// - block: The block to execute. - func eventWriteContext(bypassConsent: Bool = false, forceNewBatch: Bool = false, _ block: @escaping (DatadogContext, Writer) throws -> Void) { - self.eventWriteContext(bypassConsent: bypassConsent, forceNewBatch: forceNewBatch, block) + /// - block: The block to execute; it is called on the context queue. + func eventWriteContext(bypassConsent: Bool = false, forceNewBatch: Bool = false, _ block: @escaping (DatadogContext, Writer) -> Void) { + eventWriteContext(bypassConsent: bypassConsent, forceNewBatch: forceNewBatch, block) } } diff --git a/DatadogLogs/Sources/LogOutputs/LogFileOutput.swift b/DatadogLogs/Sources/LogOutputs/LogFileOutput.swift deleted file mode 100644 index 60edbb946a..0000000000 --- a/DatadogLogs/Sources/LogOutputs/LogFileOutput.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation -import DatadogInternal - -/// `LogOutput` writing logs to file. -internal struct LogFileOutput: LogOutput { - let fileWriter: Writer - - func write(log: LogEvent) { - fileWriter.write(value: log) - } -} diff --git a/DatadogLogs/Sources/LogOutputs/LogOutput.swift b/DatadogLogs/Sources/LogOutputs/LogOutput.swift deleted file mode 100644 index db597493d7..0000000000 --- a/DatadogLogs/Sources/LogOutputs/LogOutput.swift +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import Foundation - -/// An interface for writing logs to some destination. -internal protocol LogOutput { - func write(log: LogEvent) -} diff --git a/DatadogLogs/Tests/LogOutputs/LogFileOutputTests.swift b/DatadogLogs/Tests/LogOutputs/LogFileOutputTests.swift deleted file mode 100644 index 2e9882108f..0000000000 --- a/DatadogLogs/Tests/LogOutputs/LogFileOutputTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import TestUtilities -@testable import DatadogLogs - -class LogFileOutputTests: XCTestCase { - func testItWritesLogs() throws { - let writer = FileWriterMock() - let output = LogFileOutput(fileWriter: writer) - - output.write(log: .mockWith(status: .info, message: "log message 1")) - output.write(log: .mockWith(status: .warn, message: "log message 2")) - - let logs: [LogEvent] = writer.events() - XCTAssertEqual(logs.count, 2) - XCTAssertEqual(logs[0].status, .info) - XCTAssertEqual(logs[0].message, "log message 1") - XCTAssertEqual(logs[1].status, .warn) - XCTAssertEqual(logs[1].message, "log message 2") - } -} diff --git a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift index ccd49cad88..123730f97a 100644 --- a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift +++ b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift @@ -320,24 +320,3 @@ extension LogEvent.Attributes: Equatable { && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) } } - -/// `LogOutput` recording received logs. -class LogOutputMock: LogOutput { - var onLogRecorded: ((LogEvent) -> Void)? - - var recordedLog: LogEvent? - var allRecordedLogs: [LogEvent] = [] - - func write(log: LogEvent) { - recordedLog = log - allRecordedLogs.append(log) - onLogRecorded?(log) - } - - /// Returns newline-separated `String` description of all recorded logs. - func dumpAllRecordedLogs() -> String { - return allRecordedLogs - .map { "- \($0)" } - .joined(separator: "\n") - } -} diff --git a/DatadogTrace/Sources/DDSpan.swift b/DatadogTrace/Sources/DDSpan.swift index 24bcbe5e85..da2244b5a1 100644 --- a/DatadogTrace/Sources/DDSpan.swift +++ b/DatadogTrace/Sources/DDSpan.swift @@ -17,20 +17,24 @@ internal final class DDSpan: OTSpan { /// Writes span logs to Logging Feature. `nil` if Logging feature is disabled. private let loggingIntegration: TracingWithLoggingIntegration - /// Queue used for synchronizing mutable properties access. - private let queue: DispatchQueue - /// Unsynchronized span operation name. Must be accessed on `queue`. - private var unsafeOperationName: String - /// Unsynchronized span tags. Must be accessed on `queue`. - private var unsafeTags: [String: Encodable] - /// Unsychronized span log fields. Must be accessed on `queue`. - private var unsafeLogFields: [[String: Encodable]] - /// Unsychronized span completion. Must be accessed on `queue`. - private var unsafeIsFinished: Bool - + /// Span operation name. + @ReadWriteLock + private var operationName: String + /// Span tags. + @ReadWriteLock + private var tags: [String: Encodable] + /// Span log fields. + @ReadWriteLock + private var logFields: [[String: Encodable]] + /// If this span has completed. + @ReadWriteLock + private var isFinished: Bool + @ReadWriteLock private var activityReference: ActivityReference? - /// Telemetry interface. - private let telemetry: Telemetry + /// Builds span events. + private let eventBuilder: SpanEventBuilder + /// Writes span events to core. + private let eventWriter: SpanWriteContext init( tracer: DatadogTracer, @@ -38,18 +42,19 @@ internal final class DDSpan: OTSpan { operationName: String, startTime: Date, tags: [String: Encodable], - telemetry: Telemetry = NOPTelemetry() + eventBuilder: SpanEventBuilder, + eventWriter: SpanWriteContext ) { self.ddTracer = tracer self.ddContext = context self.startTime = startTime self.loggingIntegration = tracer.loggingIntegration - self.queue = ddTracer.queue // share the queue among all spans - self.unsafeOperationName = operationName - self.unsafeTags = tags - self.unsafeLogFields = [] - self.unsafeIsFinished = false - self.telemetry = telemetry + self.operationName = operationName + self.tags = tags + self.logFields = [] + self.isFinished = false + self.eventBuilder = eventBuilder + self.eventWriter = eventWriter } // MARK: - Open Tracing interface @@ -63,39 +68,31 @@ internal final class DDSpan: OTSpan { } func setOperationName(_ operationName: String) { - queue.async { - if self.warnIfFinished("setOperationName(_:)") { - return - } - self.unsafeOperationName = operationName + if warnIfFinished("setOperationName(_:)") { + return } + self.operationName = operationName } func setTag(key: String, value: Encodable) { - queue.async { - if self.warnIfFinished("setTag(key:value:)") { - return - } - self.unsafeTags[key] = value + if warnIfFinished("setTag(key:value:)") { + return } + _tags.mutate { $0[key] = value } } func setBaggageItem(key: String, value: String) { - queue.sync { - if self.warnIfFinished("setBaggageItem(key:value:)") { - return - } - ddContext.baggageItems.set(key: key, value: value) + if warnIfFinished("setBaggageItem(key:value:)") { + return } + ddContext.baggageItems.set(key: key, value: value) } func baggageItem(withKey key: String) -> String? { - queue.sync { - if self.warnIfFinished("baggageItem(withKey:)") { - return nil - } - return ddContext.baggageItems.get(key: key) + if warnIfFinished("baggageItem(withKey:)") { + return nil } + return ddContext.baggageItems.get(key: key) } @discardableResult @@ -108,69 +105,44 @@ internal final class DDSpan: OTSpan { } func log(fields: [String: Encodable], timestamp: Date) { - queue.async { - if self.warnIfFinished("log(fields:timestamp:)") { - return - } - self.unsafeLogFields.append(fields) + if warnIfFinished("log(fields:timestamp:)") { + return } + logFields.append(fields) sendSpanLogs(fields: fields, date: timestamp) } func finish(at time: Date) { - let isFinished: Bool = queue.sync { - let wasFinished = self.warnIfFinished("finish(at:)") - self.unsafeIsFinished = true - return wasFinished + if warnIfFinished("finish(at:)") { + return } + isFinished = true - if !isFinished { - if let activity = activityReference { - ddTracer.removeSpan(activityReference: activity) - } - sendSpan(finishTime: time, sampler: ddTracer.sampler) + if let activity = activityReference { + ddTracer.removeSpan(activityReference: activity) } + sendSpan(finishTime: time, sampler: ddTracer.sampler) } // MARK: - Writing SpanEvent /// Sends span event for given `DDSpan`. private func sendSpan(finishTime: Date, sampler: Sampler) { - guard let scope = ddTracer.core?.scope(for: TraceFeature.name) else { - return - } - - // Baggage items must be accessed outside the `tracer.queue` as it uses that queue for internal sync. - let baggageItems = ddContext.baggageItems.all - - scope.eventWriteContext { context, writer in - // This queue adds performance optimisation by reading all `unsafe*` values in one block and performing - // the `builder.createSpan()` off the main thread. This is important as the span creation includes - // attributes encoding to JSON string values (for tags and extra user info). It captures `self` strongly - // as it is very likely to be deallocated after return. - let event: SpanEvent = self.queue.sync { - let builder = SpanEventBuilder( - serviceName: self.ddTracer.service, - networkInfoEnabled: self.ddTracer.networkInfoEnabled, - eventsMapper: self.ddTracer.spanEventMapper, - telemetry: self.telemetry - ) - - return builder.createSpanEvent( - context: context, - traceID: self.ddContext.traceID, - spanID: self.ddContext.spanID, - parentSpanID: self.ddContext.parentSpanID, - operationName: self.unsafeOperationName, - startTime: self.startTime, - finishTime: finishTime, - samplingRate: sampler.samplingRate / 100.0, - isKept: sampler.sample(), - tags: self.unsafeTags, - baggageItems: baggageItems, - logFields: self.unsafeLogFields - ) - } + eventWriter.spanWriteContext { context, writer in + let event = self.eventBuilder.createSpanEvent( + context: context, + traceID: self.ddContext.traceID, + spanID: self.ddContext.spanID, + parentSpanID: self.ddContext.parentSpanID, + operationName: self.operationName, + startTime: self.startTime, + finishTime: finishTime, + samplingRate: sampler.samplingRate / 100.0, + isKept: sampler.sample(), + tags: self.tags, + baggageItems: self.ddContext.baggageItems.all, + logFields: self.logFields + ) let envelope = SpanEventsEnvelope(span: event, environment: context.env) writer.write(value: envelope) @@ -179,7 +151,7 @@ internal final class DDSpan: OTSpan { private func sendSpanLogs(fields: [String: Encodable], date: Date) { loggingIntegration.writeLog(withSpanContext: ddContext, fields: fields, date: date, else: { - self.queue.async { DD.logger.warn("The log for span \"\(self.unsafeOperationName)\" will not be send, because the Logs feature is not enabled.") } + DD.logger.warn("The log for span \"\(self.operationName)\" will not be send, because the Logs feature is not enabled.") }) } @@ -187,8 +159,8 @@ internal final class DDSpan: OTSpan { private func warnIfFinished(_ methodName: String) -> Bool { return warn( - if: unsafeIsFinished, - message: "🔥 Calling `\(methodName)` on a finished span (\"\(unsafeOperationName)\") is not allowed." + if: isFinished, + message: "🔥 Calling `\(methodName)` on a finished span (\"\(operationName)\") is not allowed." ) } } diff --git a/DatadogTrace/Sources/DDSpanContext.swift b/DatadogTrace/Sources/DDSpanContext.swift index 74aa223b8f..825403c0e6 100644 --- a/DatadogTrace/Sources/DDSpanContext.swift +++ b/DatadogTrace/Sources/DDSpanContext.swift @@ -44,7 +44,7 @@ internal class BaggageItems { } func set(key: String, value: String) { - items[key] = value + _items.mutate { $0[key] = value } } func get(key: String) -> String? { diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index 6d737634c7..6fe84d52db 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -12,13 +12,6 @@ internal class DatadogTracer: OTTracer { /// Global tags configured for Trace feature. let tags: [String: Encodable] - let service: String? - let networkInfoEnabled: Bool - let spanEventMapper: ((SpanEvent) -> SpanEvent)? - /// Queue ensuring thread-safety of the `Tracer` and `DDSpan` operations. - let queue: DispatchQueue - /// Integration with Core Context. - let contextReceiver: ContextMessageReceiver /// Integration with Logging. let loggingIntegration: TracingWithLoggingIntegration @@ -30,9 +23,8 @@ internal class DatadogTracer: OTTracer { let activeSpansPool = ActiveSpansPool() let sampler: Sampler - - /// Telemetry interface. - let telemetry: Telemetry + /// Creates span events. + let spanEventBuilder: SpanEventBuilder // MARK: - Initialization @@ -40,31 +32,18 @@ internal class DatadogTracer: OTTracer { core: DatadogCoreProtocol, sampler: Sampler, tags: [String: Encodable], - service: String?, - networkInfoEnabled: Bool, - spanEventMapper: ((SpanEvent) -> SpanEvent)?, tracingUUIDGenerator: TraceIDGenerator, dateProvider: DateProvider, - contextReceiver: ContextMessageReceiver, loggingIntegration: TracingWithLoggingIntegration, - telemetry: Telemetry = NOPTelemetry() + spanEventBuilder: SpanEventBuilder ) { self.core = core self.tags = tags - self.service = service - self.networkInfoEnabled = networkInfoEnabled - self.spanEventMapper = spanEventMapper - self.queue = DispatchQueue( - label: "com.datadoghq.tracer", - target: .global(qos: .userInteractive) - ) - self.tracingUUIDGenerator = tracingUUIDGenerator self.dateProvider = dateProvider - self.contextReceiver = contextReceiver self.loggingIntegration = loggingIntegration self.sampler = sampler - self.telemetry = telemetry + self.spanEventBuilder = spanEventBuilder } // MARK: - Open Tracing interface @@ -113,22 +92,27 @@ internal class DatadogTracer: OTTracer { } internal func startSpan(spanContext: DDSpanContext, operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { + guard let core = core else { + return DDNoopGlobals.span + } + var combinedTags = self.tags if let userTags = tags { combinedTags.merge(userTags) { $1 } } - if let rumTags = contextReceiver.context.rum { - combinedTags.merge(rumTags) { $1 } - } - + // Initialize `LazySpanWriteContext` here in `startSpan()` so it captures the `DatadogContext` valid + // for this moment of time. Added in RUM-699 to ensure spans are correctly linked with RUM information + // available on the caller thread. + let writer = LazySpanWriteContext(core: core) let span = DDSpan( tracer: self, context: spanContext, operationName: operationName, startTime: startTime ?? dateProvider.now, tags: combinedTags, - telemetry: telemetry + eventBuilder: spanEventBuilder, + eventWriter: writer ) return span } diff --git a/DatadogTrace/Sources/Feature/Baggages.swift b/DatadogTrace/Sources/Feature/Baggages.swift index e3f083f7e6..4395fda31b 100644 --- a/DatadogTrace/Sources/Feature/Baggages.swift +++ b/DatadogTrace/Sources/Feature/Baggages.swift @@ -19,7 +19,7 @@ internal struct SpanCoreContext: Encodable { } /// The RUM context received from `DatadogCore`. -internal struct RUMContext: Decodable { +internal struct RUMContext: Codable { static let key = "rum" enum CodingKeys: String, CodingKey { diff --git a/DatadogTrace/Sources/Feature/MessageReceivers.swift b/DatadogTrace/Sources/Feature/MessageReceivers.swift index 1984206ef8..2b7cb2a651 100644 --- a/DatadogTrace/Sources/Feature/MessageReceivers.swift +++ b/DatadogTrace/Sources/Feature/MessageReceivers.swift @@ -8,28 +8,17 @@ import Foundation import DatadogInternal internal struct CoreContext { - /// The RUM attributes that should be added as Span tags. - /// - /// These attributes are synchronized using a read-write lock. - var rum: [String: String]? - /// Provides the history of app foreground / background states. var applicationStateHistory: AppStateHistory? } internal final class ContextMessageReceiver: FeatureMessageReceiver { - let bundleWithRumEnabled: Bool - /// The up-to-date core context. /// /// The context is synchronized using a read-write lock. @ReadWriteLock var context: CoreContext = .init() - init(bundleWithRumEnabled: Bool) { - self.bundleWithRumEnabled = bundleWithRumEnabled - } - /// Process messages receives from the bus. /// /// - Parameters: @@ -50,24 +39,6 @@ internal final class ContextMessageReceiver: FeatureMessageReceiver { private func update(context: DatadogContext, from core: DatadogCoreProtocol) -> Bool { _context.mutate { $0.applicationStateHistory = context.applicationStateHistory - - if bundleWithRumEnabled, let rum = context.baggages[RUMContext.key] { - do { - let context: RUMContext = try rum.decode() - $0.rum = [ - "_dd.application.id": context.applicationID, - "_dd.session.id": context.sessionID - ] - - $0.rum?["_dd.view.id"] = context.viewID - $0.rum?["_dd.action.id"] = context.userActionID - } catch { - core.telemetry - .error("Fails to decode RUM context from Trace", error: error) - } - } else { - $0.rum = nil - } } return true diff --git a/DatadogTrace/Sources/Feature/TraceFeature.swift b/DatadogTrace/Sources/Feature/TraceFeature.swift index 68e7eba3ff..887194370d 100644 --- a/DatadogTrace/Sources/Feature/TraceFeature.swift +++ b/DatadogTrace/Sources/Feature/TraceFeature.swift @@ -11,38 +11,39 @@ internal final class TraceFeature: DatadogRemoteFeature { static let name = "tracing" let requestBuilder: FeatureRequestBuilder - let messageReceiver: FeatureMessageReceiver + var messageReceiver: FeatureMessageReceiver { contextReceiver } + let tracer: DatadogTracer + let contextReceiver: ContextMessageReceiver init( in core: DatadogCoreProtocol, configuration: Trace.Configuration ) { - let contextReceiver = ContextMessageReceiver( - bundleWithRumEnabled: configuration.bundleWithRumEnabled - ) self.requestBuilder = TracingRequestBuilder( customIntakeURL: configuration.customEndpoint, telemetry: core.telemetry ) - self.messageReceiver = contextReceiver + self.contextReceiver = ContextMessageReceiver() self.tracer = DatadogTracer( core: core, sampler: Sampler(samplingRate: configuration.debugSDK ? 100 : configuration.sampleRate), tags: configuration.tags ?? [:], - service: configuration.service, - networkInfoEnabled: configuration.networkInfoEnabled, - spanEventMapper: configuration.eventMapper, tracingUUIDGenerator: configuration.traceIDGenerator, dateProvider: configuration.dateProvider, - contextReceiver: contextReceiver, loggingIntegration: TracingWithLoggingIntegration( core: core, service: configuration.service, networkInfoEnabled: configuration.networkInfoEnabled ), - telemetry: core.telemetry + spanEventBuilder: SpanEventBuilder( + service: configuration.service, + networkInfoEnabled: configuration.networkInfoEnabled, + eventsMapper: configuration.eventMapper, + bundleWithRUM: configuration.bundleWithRumEnabled, + telemetry: core.telemetry + ) ) // Send configuration telemetry: diff --git a/DatadogTrace/Sources/OpenTracing/OTConstants.swift b/DatadogTrace/Sources/OpenTracing/OTConstants.swift index 3b433feace..61a24acd2f 100644 --- a/DatadogTrace/Sources/OpenTracing/OTConstants.swift +++ b/DatadogTrace/Sources/OpenTracing/OTConstants.swift @@ -71,7 +71,6 @@ public struct OTTags { /// Use them as the `key` for `fields` dictionary in `span.log(fields:)`. Use the expected type for the value. /// /// See more: [Log fields table](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#log-fields-table) -/// public struct OTLogFields { /// Expected value: `String` public static let errorKind = "error.kind" diff --git a/DatadogTrace/Sources/Span/SpanEventBuilder.swift b/DatadogTrace/Sources/Span/SpanEventBuilder.swift index a32bcb0b19..c55acf6e65 100644 --- a/DatadogTrace/Sources/Span/SpanEventBuilder.swift +++ b/DatadogTrace/Sources/Span/SpanEventBuilder.swift @@ -10,13 +10,15 @@ import DatadogInternal /// Builds `SpanEvent` representation (for later serialization) from span information recorded in `DDSpan` and values received from global configuration. internal struct SpanEventBuilder { /// Service name to encode in span. - let serviceName: String? + let service: String? /// Enriches traces with network connection info. /// This means: reachability status, connection type, mobile carrier name and many more will be added to every span and span logs. /// For full list of network info attributes see `NetworkConnectionInfo` and `CarrierInfo`. let networkInfoEnabled: Bool /// Span events mapper configured by the user, `nil` if not set. let eventsMapper: SpanEventMapper? + /// If spans should be enriched with the current RUM context. + let bundleWithRUM: Bool /// Telemetry interface. let telemetry: Telemetry @@ -45,6 +47,20 @@ internal struct SpanEventBuilder { let regularTags = castValuesToString(tagsReducer.reducedSpanTags) tags.merge(regularTags) { _, regularTag in regularTag } + if bundleWithRUM { + // Enrich with RUM context + do { + if let rum: RUMContext = try context.baggages[RUMContext.key]?.decode() { + tags[SpanTags.rumApplicationID] = rum.applicationID + tags[SpanTags.rumSessionID] = rum.sessionID + tags[SpanTags.rumViewID] = rum.viewID + tags[SpanTags.rumActionID] = rum.userActionID + } + } catch let error { + telemetry.error("Failed to decode RUM context for enriching span", error: error) + } + } + // Transform user info to `SpanEvent.UserInfo` representation let spanUserInfo = SpanEvent.UserInfo( id: context.userInfo?.id, @@ -58,7 +74,7 @@ internal struct SpanEventBuilder { spanID: spanID, parentID: parentSpanID, operationName: operationName, - serviceName: serviceName ?? context.service, + serviceName: service ?? context.service, resource: tagsReducer.extractedResourceName ?? operationName, startTime: startTime.addingTimeInterval(context.serverTimeOffset), duration: finishTime.timeIntervalSince(startTime), diff --git a/DatadogTrace/Sources/Span/SpanWriteContext.swift b/DatadogTrace/Sources/Span/SpanWriteContext.swift new file mode 100644 index 0000000000..5d3caefba5 --- /dev/null +++ b/DatadogTrace/Sources/Span/SpanWriteContext.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 DatadogInternal + +/// A type providing core context and writer for writing span events. +internal protocol SpanWriteContext { + /// Requests core context and writer for writing span events. + /// - Parameter block: The block to execute; it is called on the core's context queue. + func spanWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) +} + +/// A `SpanWriteContext` that captures core context at the moment of initialization and provides it +/// later when the actual span event is constructed. +/// +/// It ensures that spans are constructed with the context valid at the moment of span creation (_start span_) +/// instead of completion (_finish span_). This enables the proper linking of attributes from other products, like +/// associating started span with the current RUM information. +internal final class LazySpanWriteContext: SpanWriteContext { + private weak var core: DatadogCoreProtocol? + + /// The core context valid at the moment of creating `LazySpanWriteContext`. + /// It doesn't require synchronization as it is accessed only from the core context queue. + private var context: DatadogContext? + + init(core: DatadogCoreProtocol) { + self.core = core + + // Capture the core context valid at the moment of initialization: + core.scope(for: TraceFeature.name)?.context { [weak self] context in + self?.context = context + } + } + + func spanWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) { + guard let scope = core?.scope(for: TraceFeature.name) else { + return + } + + // Ignore the current context and use the one captured at initialization: + scope.eventWriteContext(bypassConsent: false, forceNewBatch: false) { _, writer in + guard let context = self.context else { + return // unexpected + } + block(context, writer) + } + } +} diff --git a/DatadogTrace/Sources/Trace.swift b/DatadogTrace/Sources/Trace.swift index 46f830fc00..81b789d4c6 100644 --- a/DatadogTrace/Sources/Trace.swift +++ b/DatadogTrace/Sources/Trace.swift @@ -55,7 +55,7 @@ public enum Trace { let urlSessionHandler = TracingURLSessionHandler( tracer: trace.tracer, - contextReceiver: trace.tracer.contextReceiver, + contextReceiver: trace.contextReceiver, tracingSampler: distributedTraceSampler, firstPartyHosts: firstPartyHosts ) diff --git a/DatadogTrace/Sources/Tracer.swift b/DatadogTrace/Sources/Tracer.swift index fd3431f569..71c798d86d 100644 --- a/DatadogTrace/Sources/Tracer.swift +++ b/DatadogTrace/Sources/Tracer.swift @@ -22,12 +22,21 @@ public enum SpanTags { /// Internal tag. `Bool` value. /// `true` if span was started or ended while the app was not active, `false` otherwise. internal static let isBackground = "is_background" - - /// Those keys used to encode information received from the user through `OpenTracingLogFields`, `OpenTracingTagKeys` or custom fields. - /// Supported by Datadog platform. - internal static let errorType = "error.type" + /// Internal tag used to encode error type received from the user through `OTLogFields`. + internal static let errorType = "error.type" + /// Internal tag used to encode error message received from the user through `OTLogFields`. internal static let errorMessage = "error.msg" - internal static let errorStack = "error.stack" + /// Internal tag used to encode error stack received from the user through `OTLogFields`. + internal static let errorStack = "error.stack" + + /// Internal tag used to encode the RUM application ID, linking the span to the current RUM session. + internal static let rumApplicationID = "_dd.application.id" + /// Internal tag used to encode the RUM session ID, linking the span to the current RUM session. + internal static let rumSessionID = "_dd.session.id" + /// Internal tag used to encode the RUM view ID, linking the span to the current RUM session. + internal static let rumViewID = "_dd.view.id" + /// Internal tag used to encode the RUM action ID, linking the span to the current RUM session. + internal static let rumActionID = "_dd.action.id" } /// A class for manual interaction with the Trace feature. It records spans that are sent to Datadog APM. diff --git a/DatadogTrace/Tests/ContextMessageReceiverTests.swift b/DatadogTrace/Tests/ContextMessageReceiverTests.swift index 794b8de904..d8cb9aeb3b 100644 --- a/DatadogTrace/Tests/ContextMessageReceiverTests.swift +++ b/DatadogTrace/Tests/ContextMessageReceiverTests.swift @@ -13,7 +13,7 @@ import DatadogInternal class ContextMessageReceiverTests: XCTestCase { func testItReceivesApplicationStateHistory() throws { // Given - let receiver = ContextMessageReceiver(bundleWithRumEnabled: .mockRandom()) + let receiver = ContextMessageReceiver() let core = PassthroughCoreMock( context: .mockWith(applicationStateHistory: .mockAppInBackground()), messageReceiver: receiver @@ -27,139 +27,4 @@ class ContextMessageReceiverTests: XCTestCase { // Then XCTAssertEqual(receiver.context.applicationStateHistory?.currentSnapshot.state, .active) } - - func testItReceivesRUMContext() throws { - // Given - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) - let core = PassthroughCoreMock() - - let coreContext1: DatadogContext = .mockWith( - baggages: [ - "rum": .init([ - "application.id": "app-id", - "session.id": "session-id", - "view.id": "view-id", - "user_action.id": "action-id" - ]) - ] - ) - - let coreContext2: DatadogContext = .mockWith( - baggages: [ - "rum": .init([ - "application.id": "app-id", - "session.id": "session-id", - "view.id": nil, - "user_action.id": nil - ]) - ] - ) - - // When - XCTAssert( - receiver.receive(message: .context(coreContext1), from: core) - ) - - // Then - XCTAssertEqual(receiver.context.rum?["_dd.application.id"], "app-id") - XCTAssertEqual(receiver.context.rum?["_dd.session.id"], "session-id") - XCTAssertEqual(receiver.context.rum?["_dd.view.id"], "view-id") - XCTAssertEqual(receiver.context.rum?["_dd.action.id"], "action-id") - - // When - XCTAssert( - receiver.receive(message: .context(coreContext2), from: core) - ) - - // Then - XCTAssertEqual(receiver.context.rum?["_dd.application.id"], "app-id") - XCTAssertEqual(receiver.context.rum?["_dd.session.id"], "session-id") - XCTAssertNil(receiver.context.rum?["_dd.view.id"]) - XCTAssertNil(receiver.context.rum?["_dd.action.id"]) - } - - func testItReceivesNilRUMContext() throws { - // Given - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) - let core = PassthroughCoreMock() - - let coreContext: DatadogContext = .mockWith( - baggages: [ - "rum": .init([ - "application.id": "app-id", - "session.id": "session-id", - "view.id": "view-id", - "user_action.id": "action-id" - ]) - ] - ) - - // When - XCTAssert( - receiver.receive(message: .context(coreContext), from: core) - ) - - // Then - XCTAssertEqual(receiver.context.rum?["_dd.application.id"], "app-id") - XCTAssertEqual(receiver.context.rum?["_dd.session.id"], "session-id") - XCTAssertEqual(receiver.context.rum?["_dd.view.id"], "view-id") - XCTAssertEqual(receiver.context.rum?["_dd.action.id"], "action-id") - - // When - XCTAssert( - receiver.receive(message: .context(.mockAny()), from: core) - ) - - // Then - XCTAssertNil(receiver.context.rum) - } - - func testItReceivesMalformedRUMContext() throws { - // Given - let telemetryReceiver = TelemetryReceiverMock() - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) - let core = PassthroughCoreMock( - messageReceiver: telemetryReceiver - ) - - let coreContext: DatadogContext = .mockWith( - baggages: [ - "rum": .init("malformed RUM context") - ] - ) - - // When - XCTAssert( - receiver.receive(message: .context(coreContext), from: core) - ) - - // Then - XCTAssertNil(receiver.context.rum) - - let error = try XCTUnwrap(telemetryReceiver.messages.first?.asError) - XCTAssert(error.message.contains("Fails to decode RUM context from Trace - typeMismatch")) - } - - func testItIngnoresRUMContext() throws { - // Given - let receiver = ContextMessageReceiver(bundleWithRumEnabled: false) - let core = PassthroughCoreMock() - let coreContext: DatadogContext = .mockWith( - baggages: [ - "rum": .init([ - "application.id": "app-id", - "session.id": "session-id", - "view.id": "view-id", - "user_action.id": "action-id" - ]) - ] - ) - - // When - XCTAssert( - receiver.receive(message: .context(coreContext), from: core) - ) - - XCTAssertNil(receiver.context.rum) - } } diff --git a/DatadogTrace/Tests/DDSpanTests.swift b/DatadogTrace/Tests/DDSpanTests.swift index 8e76e881ca..c2da1b0d57 100644 --- a/DatadogTrace/Tests/DDSpanTests.swift +++ b/DatadogTrace/Tests/DDSpanTests.swift @@ -19,7 +19,7 @@ class DDSpanTests: XCTestCase { // Given let tracer: DatadogTracer = .mockWith(core: core) - let span: DDSpan = .mockWith(tracer: tracer) + let span = tracer.startSpan(operationName: .mockAny()) // When span.finish() @@ -38,8 +38,8 @@ class DDSpanTests: XCTestCase { // Given let defaultOperationName: String = .mockRandom() let tracer: DatadogTracer = .mockWith(core: core) - let defaultSpan: DDSpan = .mockWith(tracer: tracer, operationName: defaultOperationName) - let customizedSpan: DDSpan = .mockWith(tracer: tracer, operationName: defaultOperationName) + let defaultSpan = tracer.startSpan(operationName: defaultOperationName) + let customizedSpan = tracer.startSpan(operationName: defaultOperationName) // When let customizedOperationName: String = .mockRandom() @@ -64,8 +64,8 @@ class DDSpanTests: XCTestCase { // Given let defaultTags: [String: String] = .mockRandom() let tracer: DatadogTracer = .mockWith(core: core) - let defaultSpan: DDSpan = .mockWith(tracer: tracer, tags: defaultTags) - let customizedSpan: DDSpan = .mockWith(tracer: tracer, tags: defaultTags) + let defaultSpan = tracer.startSpan(operationName: .mockAny(), tags: defaultTags) + let customizedSpan = tracer.startSpan(operationName: .mockAny(), tags: defaultTags) // When let customTags: [String: String] = .mockRandom() @@ -110,7 +110,7 @@ class DDSpanTests: XCTestCase { // Given let tracer: DatadogTracer = .mockWith(core: core) - let span: DDSpan = .mockWith(tracer: tracer) + let span = tracer.startSpan(operationName: .mockAny()) // When callConcurrently( @@ -141,10 +141,8 @@ class DDSpanTests: XCTestCase { defer { dd.reset() } let core = PassthroughCoreMock(messageReceiver: FeatureMessageReceiverMock()) - let span: DDSpan = .mockWith( - tracer: .mockWith(core: core), - operationName: "the span" - ) + let tracer: DatadogTracer = .mockWith(core: core) + let span = tracer.startSpan(operationName: "the span") span.finish() let fixtures: [(() -> Void, String)] = [ @@ -164,7 +162,6 @@ class DDSpanTests: XCTestCase { fixtures.forEach { tracerMethod, expectedConsoleWarning in tracerMethod() - span.tracer().dd.queue.sync {} // wait synchronizing span's internal state XCTAssertEqual(dd.logger.warnLog?.message, expectedConsoleWarning) } } diff --git a/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift b/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift index f60263be0c..9e67653422 100644 --- a/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift +++ b/DatadogTrace/Tests/Span/SpanEventBuilderTests.swift @@ -13,7 +13,7 @@ import DatadogInternal class SpanEventBuilderTests: XCTestCase { func testBuildingBasicSpan() { let context: DatadogContext = .mockWith(sdkVersion: "1.2.3") - let builder: SpanEventBuilder = .mockWith(serviceName: "test-service-name") + let builder: SpanEventBuilder = .mockWith(service: "test-service-name") let span = builder.createSpanEvent( context: context, @@ -499,4 +499,103 @@ class SpanEventBuilderTests: XCTestCase { ) XCTAssertEqual(dd.logger.errorLog?.error?.message, "Value cannot be encoded.") } + + // MARK: - RUM context enrichment + + // swiftlint:disable opening_brace + func testWhenBundleWithRUMisEnabled_itCreatesSpanWithRUMContext() { + // Given + let rum = oneOf([ + { RUMContext(applicationID: .mockRandom(), sessionID: .mockRandom(), viewID: .mockRandom(), userActionID: .mockRandom()) }, + { RUMContext(applicationID: .mockRandom(), sessionID: .mockRandom(), viewID: .mockRandom(), userActionID: nil) }, + { RUMContext(applicationID: .mockRandom(), sessionID: .mockRandom(), viewID: nil, userActionID: nil) } + ]) + let context: DatadogContext = .mockWith(baggages: [RUMContext.key: .init(rum)]) + + // When + let builder: SpanEventBuilder = .mockWith(bundleWithRUM: true) + let span = builder.createSpanEvent( + context: context, + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [:], + baggageItems: [:], + logFields: [] + ) + + // Then + XCTAssertEqual(span.tags[SpanTags.rumApplicationID], rum.applicationID) + XCTAssertEqual(span.tags[SpanTags.rumSessionID], rum.sessionID) + XCTAssertEqual(span.tags[SpanTags.rumViewID], rum.viewID) + XCTAssertEqual(span.tags[SpanTags.rumActionID], rum.userActionID) + } + // swiftlint:enable opening_brace + + func testWhenBundleWithRUMisDisabled_itCreatesSpanWithNoRUMContext() { + // Given + let rum = RUMContext( + applicationID: .mockRandom(), + sessionID: .mockRandom(), + viewID: .mockRandom(), + userActionID: .mockRandom() + ) + let context: DatadogContext = .mockWith(baggages: [RUMContext.key: .init(rum)]) + + // When + let builder: SpanEventBuilder = .mockWith(bundleWithRUM: false) + let span = builder.createSpanEvent( + context: context, + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [:], + baggageItems: [:], + logFields: [] + ) + + // Then + XCTAssertNil(span.tags[SpanTags.rumApplicationID]) + XCTAssertNil(span.tags[SpanTags.rumSessionID]) + XCTAssertNil(span.tags[SpanTags.rumViewID]) + XCTAssertNil(span.tags[SpanTags.rumActionID]) + } + + func testWhenBundleWithRUMisEnabledAndRUMBaggageIsMalformed_itSendsTelemetryError() throws { + let telemetry = TelemetryMock() + + // Given + let context: DatadogContext = .mockWith(baggages: [RUMContext.key: .init("malformed RUM context")]) + + // When + let builder: SpanEventBuilder = .mockWith(bundleWithRUM: true, telemetry: telemetry) + _ = builder.createSpanEvent( + context: context, + traceID: .mockAny(), + spanID: .mockAny(), + parentSpanID: .mockAny(), + operationName: .mockAny(), + startTime: .mockAny(), + finishTime: .mockAny(), + samplingRate: .mockAny(), + isKept: .mockAny(), + tags: [:], + baggageItems: [:], + logFields: [] + ) + + // Then + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertTrue(error.message.hasPrefix("Failed to decode RUM context for enriching span")) + } } diff --git a/DatadogTrace/Tests/Span/SpanWriteContextTests.swift b/DatadogTrace/Tests/Span/SpanWriteContextTests.swift new file mode 100644 index 0000000000..b366ed0631 --- /dev/null +++ b/DatadogTrace/Tests/Span/SpanWriteContextTests.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 XCTest +import TestUtilities +import DatadogInternal +@testable import DatadogTrace + +class SpanWriteContextTests: XCTestCase { + func testWhenRequestingSpanWriteContext_itProvidesInitialCoreContext() { + let retrieveContext = expectation(description: "provide core context") + let writeEvent = expectation(description: "write event to core") + + let initialContext: DatadogContext = .mockRandom() + + // Given + let core = PassthroughCoreMock(context: initialContext, expectation: writeEvent) + let writer = LazySpanWriteContext(core: core) + + // When + core.context = .mockRandom() + writer.spanWriteContext { providedContext, _ in + // Then + DDAssertReflectionEqual(providedContext, initialContext) + retrieveContext.fulfill() + } + + waitForExpectations(timeout: 0.5) + } + + func testWhenWritingEvent_itRespectsCoreConsentAndBatching() { + let core = PassthroughCoreMock( + expectation: expectation(description: "write event to core"), + bypassConsentExpectation: invertedExpectation(description: "do not bypass consent"), + forceNewBatchExpectation: invertedExpectation(description: "do not force new batch") + ) + + // Given + let writer = LazySpanWriteContext(core: core) + + // When + writer.spanWriteContext { _, writer in + writer.write(value: SpanEvent.mockAny()) + } + + waitForExpectations(timeout: 0.5) + } +} diff --git a/DatadogTrace/Tests/TraceTests.swift b/DatadogTrace/Tests/TraceTests.swift index b778fdafcd..0aab78ef1f 100644 --- a/DatadogTrace/Tests/TraceTests.swift +++ b/DatadogTrace/Tests/TraceTests.swift @@ -66,12 +66,12 @@ class TraceTests: XCTestCase { let tracer = Tracer.shared(in: core).dd let trace = try XCTUnwrap(core.get(feature: TraceFeature.self)) XCTAssertEqual(tracer.sampler.samplingRate, 100) - XCTAssertNil(tracer.service) + XCTAssertNil(tracer.spanEventBuilder.service) XCTAssertNil(tracer.loggingIntegration.service) XCTAssertTrue(tracer.tags.isEmpty) XCTAssertNil(core.get(feature: NetworkInstrumentationFeature.self)) - XCTAssertEqual(tracer.networkInfoEnabled, false) - XCTAssertNil(tracer.spanEventMapper) + XCTAssertEqual(tracer.spanEventBuilder.networkInfoEnabled, false) + XCTAssertNil(tracer.spanEventBuilder.eventsMapper) XCTAssertNil((trace.requestBuilder as? TracingRequestBuilder)?.customIntakeURL) } @@ -98,7 +98,7 @@ class TraceTests: XCTestCase { // Then let tracer = Tracer.shared(in: core).dd - XCTAssertEqual(tracer.service, random) + XCTAssertEqual(tracer.spanEventBuilder.service, random) XCTAssertEqual(tracer.loggingIntegration.service, random) } @@ -181,8 +181,8 @@ class TraceTests: XCTestCase { Trace.enable(with: config, in: core) // Then - let trace = try XCTUnwrap(core.get(feature: TraceFeature.self)) - XCTAssertEqual((trace.messageReceiver as? ContextMessageReceiver)?.bundleWithRumEnabled, random) + let tracer = Tracer.shared(in: core).dd + XCTAssertEqual(tracer.spanEventBuilder.bundleWithRUM, random) } func testWhenEnabledWithSendNetworkInfo() { @@ -195,7 +195,7 @@ class TraceTests: XCTestCase { // Then let tracer = Tracer.shared(in: core).dd - XCTAssertEqual(tracer.networkInfoEnabled, random) + XCTAssertEqual(tracer.spanEventBuilder.networkInfoEnabled, random) XCTAssertEqual(tracer.loggingIntegration.networkInfoEnabled, random) } @@ -209,7 +209,7 @@ class TraceTests: XCTestCase { // Then let tracer = Tracer.shared(in: core).dd - XCTAssertEqual(tracer.networkInfoEnabled, random) + XCTAssertEqual(tracer.spanEventBuilder.networkInfoEnabled, random) XCTAssertEqual(tracer.loggingIntegration.networkInfoEnabled, random) } diff --git a/DatadogTrace/Tests/TracingFeatureMocks.swift b/DatadogTrace/Tests/TracingFeatureMocks.swift index 7ad92b6778..8622275ab3 100644 --- a/DatadogTrace/Tests/TracingFeatureMocks.swift +++ b/DatadogTrace/Tests/TracingFeatureMocks.swift @@ -38,6 +38,10 @@ extension BaggageItems { } } +internal struct NOPSpanWriteContext: SpanWriteContext { + func spanWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) {} +} + extension DDSpan { static func mockAny(in core: DatadogCoreProtocol) -> DDSpan { return mockWith(core: core) @@ -48,14 +52,18 @@ extension DDSpan { context: DDSpanContext = .mockAny(), operationName: String = .mockAny(), startTime: Date = .mockAny(), - tags: [String: Encodable] = [:] + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() ) -> DDSpan { return DDSpan( tracer: tracer, context: context, operationName: operationName, startTime: startTime, - tags: tags + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter ) } @@ -64,14 +72,18 @@ extension DDSpan { context: DDSpanContext = .mockAny(), operationName: String = .mockAny(), startTime: Date = .mockAny(), - tags: [String: Encodable] = [:] + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() ) -> DDSpan { return DDSpan( tracer: .mockAny(in: core), context: context, operationName: operationName, startTime: startTime, - tags: tags + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter ) } } @@ -186,25 +198,19 @@ extension DatadogTracer { core: DatadogCoreProtocol, sampler: Sampler = .mockKeepAll(), tags: [String: Encodable] = [:], - service: String? = nil, - networkInfoEnabled: Bool = true, - spanEventMapper: ((SpanEvent) -> SpanEvent)? = nil, tracingUUIDGenerator: TraceIDGenerator = DefaultTraceIDGenerator(), dateProvider: DateProvider = SystemDateProvider(), - contextReceiver: ContextMessageReceiver = .mockAny(), + spanEventBuilder: SpanEventBuilder = .mockAny(), loggingIntegration: TracingWithLoggingIntegration = .mockAny() ) -> DatadogTracer { return DatadogTracer( core: core, sampler: sampler, tags: tags, - service: service, - networkInfoEnabled: networkInfoEnabled, - spanEventMapper: spanEventMapper, tracingUUIDGenerator: tracingUUIDGenerator, dateProvider: dateProvider, - contextReceiver: contextReceiver, - loggingIntegration: loggingIntegration + loggingIntegration: loggingIntegration, + spanEventBuilder: spanEventBuilder ) } } @@ -221,7 +227,7 @@ extension TracingWithLoggingIntegration { extension ContextMessageReceiver { static func mockAny() -> ContextMessageReceiver { - return ContextMessageReceiver(bundleWithRumEnabled: true) + return ContextMessageReceiver() } } @@ -231,15 +237,17 @@ extension SpanEventBuilder { } static func mockWith( - serviceName: String = .mockAny(), + service: String = .mockAny(), networkInfoEnabled: Bool = false, eventsMapper: SpanEventMapper? = nil, + bundleWithRUM: Bool = false, telemetry: Telemetry = NOPTelemetry() ) -> SpanEventBuilder { return SpanEventBuilder( - serviceName: serviceName, + service: service, networkInfoEnabled: networkInfoEnabled, eventsMapper: eventsMapper, + bundleWithRUM: bundleWithRUM, telemetry: telemetry ) } diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index 38439854bb..9b70ac5fac 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -19,7 +19,7 @@ class TracingURLSessionHandlerTests: XCTestCase { override func setUp() { super.setUp() - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) + let receiver = ContextMessageReceiver() core = PassthroughCoreMock(messageReceiver: receiver) tracer = .mockWith( @@ -46,7 +46,7 @@ class TracingURLSessionHandlerTests: XCTestCase { // Given let handler = TracingURLSessionHandler( tracer: tracer, - contextReceiver: ContextMessageReceiver(bundleWithRumEnabled: true), + contextReceiver: ContextMessageReceiver(), tracingSampler: .mockKeepAll(), firstPartyHosts: .init() ) @@ -77,7 +77,7 @@ class TracingURLSessionHandlerTests: XCTestCase { // Given let handler = TracingURLSessionHandler( tracer: tracer, - contextReceiver: ContextMessageReceiver(bundleWithRumEnabled: true), + contextReceiver: ContextMessageReceiver(), tracingSampler: .mockRejectAll(), firstPartyHosts: .init() ) @@ -262,7 +262,7 @@ class TracingURLSessionHandlerTests: XCTestCase { core.expectation?.isInverted = true // Given - let receiver = ContextMessageReceiver(bundleWithRumEnabled: true) + let receiver = ContextMessageReceiver() let handler = TracingURLSessionHandler( tracer: .mockWith(core: core), diff --git a/DatadogTrace/Tests/Utils/ActiveSpansPoolTests.swift b/DatadogTrace/Tests/Utils/ActiveSpansPoolTests.swift index 513b170971..3c03408206 100644 --- a/DatadogTrace/Tests/Utils/ActiveSpansPoolTests.swift +++ b/DatadogTrace/Tests/Utils/ActiveSpansPoolTests.swift @@ -11,8 +11,18 @@ import DatadogInternal @testable import DatadogTrace class ActiveSpansPoolTests: XCTestCase { + private var core: DatadogCoreProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUpWithError() throws { + core = PassthroughCoreMock() + } + + override func tearDown() { + core = nil + } + func testsWhenSpanIsStartedIsAssignedToActiveSpan() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) let previousSpan = tracer.activeSpan XCTAssertNil(previousSpan) @@ -23,7 +33,7 @@ class ActiveSpansPoolTests: XCTestCase { } func testsWhenSpanIsFinishedIsRemovedFromActiveSpan() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) XCTAssertNil(tracer.activeSpan) let oneSpan = tracer.startSpan(operationName: .mockAny()).setActive() @@ -34,7 +44,7 @@ class ActiveSpansPoolTests: XCTestCase { } func testsSpanWithoutParentInheritsActiveSpan() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) let firstSpan = tracer.startSpan(operationName: .mockAny()) firstSpan.setActive() let previousActiveSpan = tracer.activeSpan @@ -51,7 +61,7 @@ class ActiveSpansPoolTests: XCTestCase { } func testsSpanWithParentDoesntInheritActiveSpan() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) let oneSpan = tracer.startSpan(operationName: .mockAny()) let otherSpan = tracer.startSpan(operationName: .mockAny()).setActive() @@ -65,7 +75,7 @@ class ActiveSpansPoolTests: XCTestCase { } func testActiveSpanIsKeptPerTask() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) let oneSpan = tracer.startSpan(operationName: .mockAny()).setActive() var firstSpan: OTSpan? var secondSpan: OTSpan? @@ -95,7 +105,7 @@ class ActiveSpansPoolTests: XCTestCase { } func testsSetActiveSpanCalledMultipleTimes() throws { - let tracer = DatadogTracer.mockAny(in: PassthroughCoreMock()) + let tracer = DatadogTracer.mockAny(in: core) defer { tracer.activeSpansPool.destroy() } let firstSpan = tracer.startSpan(operationName: .mockAny()).setActive() diff --git a/TestUtilities/Helpers/XCTestCase.swift b/TestUtilities/Helpers/XCTestCase.swift index 40ebd9d97f..9b7d43edf9 100644 --- a/TestUtilities/Helpers/XCTestCase.swift +++ b/TestUtilities/Helpers/XCTestCase.swift @@ -63,4 +63,13 @@ extension XCTestCase { ) #endif } + + /// Creates and returns an expectation associated with the test case by setting `isInverted` to `true`. + /// - Parameter description: This string will be displayed in the test log to help diagnose failures. + /// - Returns: Inverted expectation. + public func invertedExpectation(description: String) -> XCTestExpectation { + let expectation = self.expectation(description: description) + expectation.isInverted = true + return expectation + } } diff --git a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift index 7d4f9588b9..b09814f17b 100644 --- a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift +++ b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift @@ -112,10 +112,7 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { /// Execute `block` with the current context and a `writer` to record events. /// /// - Parameter block: The block to execute. - public func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: (DatadogContext, Writer) throws -> Void) { - XCTAssertNoThrow(try block(context, writer), "Encountered an error when executing `eventWriteContext`") - expectation?.fulfill() - + public func eventWriteContext(bypassConsent: Bool, forceNewBatch: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { if bypassConsent { bypassConsentExpectation?.fulfill() } @@ -123,6 +120,13 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { if forceNewBatch { forceNewBatchExpectation?.fulfill() } + + block(context, writer) + expectation?.fulfill() + } + + public func context(_ block: @escaping (DatadogContext) -> Void) { + block(context) } /// Recorded events from feature scopes. From 2ecfae1c51cafb49723417b2e5545904f2483bf5 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 11 Jan 2024 10:44:15 +0100 Subject: [PATCH 21/60] RUM-699 Fix CHANGELOG after rebase --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603a0536d3..dd4820e0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Unreleased +- [FIX] RUM session not being linked to spans. See [#1615][] + # 2.6.0 / 09-01-2024 - [FEATURE] Add `currentSessionID(completion:)` accessor to access the current session ID. - [FEATURE] Add `BatchProcessingLevel` configuration allowing to process more batches within single read/upload cycle. See [#1531][] -- [FIX] Use `currentRequest` instead `originalRequest` for URLSession request interception -- [FIX] Remove weak `UIViewController` references. See [#1597][] - [FIX] Use `currentRequest` instead `originalRequest` for URLSession request interception. See [#1609][] -- [FIX] RUM session not being linked to spans. See [#1615][] +- [FIX] Remove weak `UIViewController` references. See [#1597][] # 2.5.1 / 20-12-2023 @@ -572,8 +572,8 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1533]: https://github.com/DataDog/dd-sdk-ios/pull/1533 [#1594]: https://github.com/DataDog/dd-sdk-ios/pull/1594 [#1536]: https://github.com/DataDog/dd-sdk-ios/pull/1536 -[#1609]: [https://github.com/DataDog/dd-sdk-ios/pull/1609] -[#1615]: [https://github.com/DataDog/dd-sdk-ios/pull/1615] +[#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609 +[#1615]: https://github.com/DataDog/dd-sdk-ios/pull/1615 [#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 [#1596]: https://github.com/DataDog/dd-sdk-ios/pull/1596 [#1597]: https://github.com/DataDog/dd-sdk-ios/pull/1597 From c37be3775936ae38ef6ba3081df7548d940d9c16 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 11 Jan 2024 12:21:43 +0100 Subject: [PATCH 22/60] GH1618 Remove compiler macro in (internal) flushing logic --- DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift index b106a409c9..f438a94539 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -157,13 +157,11 @@ internal class FilesOrchestrator: FilesOrchestratorType { .compactMap { try deleteFileIfItsObsolete(file: $0.file, fileCreationDate: $0.creationDate) } .sorted(by: { $0.creationDate < $1.creationDate }) - #if DD_SDK_COMPILED_FOR_TESTING if ignoreFilesAgeWhenReading { return filesFromOldest .prefix(limit) .map { $0.file } } - #endif let filtered = filesFromOldest .filter { From ce479f18d0d02fa67a4065fe49eaaf8f20e168dd Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Tue, 7 Nov 2023 12:46:44 +0100 Subject: [PATCH 23/60] RUM-1040 Stop core instance --- DatadogCore/Sources/Core/DatadogCore.swift | 7 +++- DatadogCore/Sources/Datadog.swift | 17 +++++--- .../DatadogCore/DatadogCoreTests.swift | 39 +++++++++++++++++-- DatadogCore/Tests/Datadog/DatadogTests.swift | 18 +++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 6d0bca0804..1a0a7f3121 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -207,7 +207,12 @@ internal final class DatadogCore { allUploads.forEach { $0.flushAndTearDown() } allStorages.forEach { $0.setIgnoreFilesAgeWhenReading(to: false) } - // Deallocate all Features and their storage & upload units: + stop() + } + + /// Stops all processes for this instance of the Datadog core by + /// deallocating all Features and their storage & upload units. + func stop() { stores = [:] features = [:] } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 425f0203bd..1361bff8f9 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -301,6 +301,14 @@ public enum Datadog { core?.clearAllData() } + /// Stops the initialized SDK instance attached to the given name. + /// + /// - Parameter instanceName: The core instance name + public static func stopInstance(named instanceName: String = CoreRegistry.defaultInstanceName) { + let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore + core?.stop() + } + /// Initializes the Datadog SDK. /// /// You **must** initialize the core instance of the Datadog SDK prior to enabling any Product. @@ -489,13 +497,10 @@ public enum Datadog { #endif internal static func internalFlushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { - assert(CoreRegistry.instance(named: instanceName) is DatadogCore, "SDK must be first initialized.") - + // Unregister core instance: + let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore // Flush and tear down SDK core: - (CoreRegistry.instance(named: instanceName) as? DatadogCore)?.flushAndTearDown() - - // Deinitialize `Datadog`: - CoreRegistry.unregisterInstance(named: instanceName) + core?.flushAndTearDown() } } diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift index 081d4dd2d1..021f9f91aa 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -46,7 +46,6 @@ class DatadogCoreTests: XCTestCase { maxBatchesPerUpload: .mockRandom(min: 1, max: 100), backgroundTasksEnabled: .mockAny() ) - defer { core.flushAndTearDown() } let requestBuilderSpy = FeatureRequestBuilderSpy() try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) @@ -93,7 +92,6 @@ class DatadogCoreTests: XCTestCase { maxBatchesPerUpload: .mockRandom(min: 1, max: 100), backgroundTasksEnabled: .mockAny() ) - defer { core.flushAndTearDown() } let requestBuilderSpy = FeatureRequestBuilderSpy() try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) @@ -148,7 +146,6 @@ class DatadogCoreTests: XCTestCase { maxBatchesPerUpload: .mockRandom(min: 1, max: 100), backgroundTasksEnabled: .mockAny() ) - defer { core.flushAndTearDown() } let requestBuilderSpy = FeatureRequestBuilderSpy() try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) @@ -293,4 +290,40 @@ class DatadogCoreTests: XCTestCase { XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.maxFileAgeForWrite, 95) XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.minFileAgeForRead, 105) } + + func testWhenStoppingInstance_itDoesNotUploadEvents() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockAny(), + backgroundTasksEnabled: .mockAny() + ) + + let requestBuilderSpy = FeatureRequestBuilderSpy() + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + let scope = try XCTUnwrap(core.scope(for: FeatureMock.name)) + + // When + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "should not be sent")) + } + + core.stop() + + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "should not be sent")) + } + + // Then + XCTAssertNil(core.scope(for: FeatureMock.name)) + core.flushAndTearDown() + XCTAssertEqual(requestBuilderSpy.requestParameters.count, 0, "It should not send any request") + } } diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index 749aa57d13..84dd88daf5 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -418,6 +418,24 @@ class DatadogTests: XCTestCase { XCTAssertTrue(CoreRegistry.instance(named: "test") is DatadogCore) } + func testStopSDKInstance() throws { + // Given + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom(), + instanceName: "test" + ) + + // Then + XCTAssertTrue(CoreRegistry.instance(named: "test") is DatadogCore) + + // When + Datadog.stopInstance(named: "test") + + // Then + XCTAssertTrue(CoreRegistry.instance(named: "test") is NOPDatadogCore) + } + func testGivenDefaultSDKInstanceInitialized_customOneCanBeInitializedAfterIt() throws { let defaultConfig = Datadog.Configuration(clientToken: "abc-123", env: "default") let customConfig = Datadog.Configuration(clientToken: "def-456", env: "custom") From 63d237738d90be9d54dc0f9007ee053b511dcb41 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Tue, 9 Jan 2024 14:31:08 +0100 Subject: [PATCH 24/60] RUM-1040 Add integration test scenario --- .../Core/StopCoreScenarioTests.swift | 185 +++++++++++ .../Scenarios/RUM/RUMCommonAsserts.swift | 2 +- .../project.pbxproj | 62 +++- .../Runner/AppConfiguration.swift | 52 ++- ...pleAppDelegate.swift => AppDelegate.swift} | 42 +-- .../Runner/Scenarios/Core/CoreScenarios.swift | 59 ++++ .../CSHomeViewController.swift | 22 ++ .../CSPictureViewController.swift | 53 +++ .../CSRootViewController.swift | 19 ++ .../StopCoreScenario.storyboard | 303 ++++++++++++++++++ .../SendLogsFixtureViewController.swift | 22 +- .../SendTracesFixtureViewController.swift | 2 + .../TSHomeViewController.swift | 6 +- .../URLSession/URLSessionScenarios.swift | 5 - .../Utils/CustomURLSessionDelegate.swift | 12 + .../Sources/HTTPServerMock/ServerMock.swift | 6 + .../python/start_mock_server.py | 21 ++ 17 files changed, 807 insertions(+), 66 deletions(-) create mode 100644 IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift rename IntegrationTests/Runner/{ExampleAppDelegate.swift => AppDelegate.swift} (53%) create mode 100644 IntegrationTests/Runner/Scenarios/Core/CoreScenarios.swift create mode 100644 IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSHomeViewController.swift create mode 100644 IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSPictureViewController.swift create mode 100644 IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSRootViewController.swift create mode 100644 IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/StopCoreScenario.storyboard create mode 100644 IntegrationTests/Runner/Utils/CustomURLSessionDelegate.swift diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift new file mode 100644 index 0000000000..6a37c55f3a --- /dev/null +++ b/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift @@ -0,0 +1,185 @@ +/* + * 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 HTTPServerMock +import XCTest + +private class CSRootScreen: XCUIApplication { + func startCore() { + buttons["Start Core"].tap() + } + + func stopCore() { + buttons["Stop Core"].tap() + } + + func tapGoToHome() -> CSHomeScreen { + buttons["Go To Home"].tap() + return CSHomeScreen() + } +} + +private class CSHomeScreen: XCUIApplication { + func tapTestLogging() { + buttons["Test Logging"].tap() + } + + func tapTestTracing() { + buttons["Test Tracing"].tap() + } + + func tapTestRUM() -> CSPictureScreen { + buttons["Test RUM"].tap() + return CSPictureScreen() + } + + func tapBack() -> CSRootScreen { + navigationBars["Runner.CSHomeView"].buttons["Back"].tap() + return CSRootScreen() + } +} + +private class CSPictureScreen: XCUIApplication { + func tapDownloadImage() { + buttons["Download image"].tap() + } + + func waitForImageBeingDownloaded() { + _ = staticTexts["☑️"].waitForExistence(timeout: 10) + } + + func tapBack() -> CSHomeScreen { + navigationBars["Runner.CSPictureView"].buttons["Back"].tap() + return CSHomeScreen() + } +} + +class StopCoreScenarioTests: IntegrationTests, LoggingCommonAsserts, TracingCommonAsserts, RUMCommonAsserts { + func testStartAndStopCoreInstance() throws { + let loggingServerSession = server.obtainUniqueRecordingSession() + let tracingServerSession = server.obtainUniqueRecordingSession() + let rumServerSession = server.obtainUniqueRecordingSession() + + let app = ExampleApplication() + app.launchWith( + testScenarioClassName: "StopCoreScenario", + serverConfiguration: HTTPServerMockConfiguration( + logsEndpoint: loggingServerSession.recordingURL, + tracesEndpoint: tracingServerSession.recordingURL, + rumEndpoint: rumServerSession.recordingURL + ) + ) + + // Play scenarios + + var root = playScenario() + + try assertLoggingDataWasCollected(by: loggingServerSession) + try assertTracingDataWasCollected(by: tracingServerSession) + try assertRUMDataWasCollected(by: rumServerSession) + server.clearAllRequests() + + root.stopCore() + root = playScenario(from: root) + + Thread.sleep(forTimeInterval: dataDeliveryTimeout) + + let recordedLoggingRequests = try loggingServerSession.getRecordedRequests() + XCTAssertEqual(recordedLoggingRequests.count, 0, "No logging data should be send.") + let recordedTracingRequests = try loggingServerSession.getRecordedRequests() + XCTAssertEqual(recordedTracingRequests.count, 0, "No tracing data should be send.") + let recordedRUMRequests = try rumServerSession.getRecordedRequests() + XCTAssertEqual(recordedRUMRequests.count, 0, "No RUM data should be send.") + + root.startCore() + root = playScenario(from: root) + + try assertLoggingDataWasCollected(by: loggingServerSession) + try assertTracingDataWasCollected(by: tracingServerSession) + try assertRUMDataWasCollected(by: rumServerSession) + } + + /// Plays following scenario for started application: + /// * sends log and trace from home screen, + /// * goes to picture screen and downloads the image, + /// * goes back to the home screen. + private func playScenario(from root: CSRootScreen = CSRootScreen()) -> CSRootScreen { + let home = root.tapGoToHome() + home.tapTestLogging() + home.tapTestTracing() + let pictureScreen = home.tapTestRUM() + pictureScreen.tapDownloadImage() + pictureScreen.waitForImageBeingDownloaded() + return pictureScreen + .tapBack() + .tapBack() + } + + // MARK: - Data assertions + + private func assertLoggingDataWasCollected(by serverSession: ServerSession) throws { + let recordedRequests = try serverSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + try LogMatcher.from(requests: requests).count == 1 + } + + assertLogging(requests: recordedRequests) + + let logMatchers = try LogMatcher.from(requests: recordedRequests) + XCTAssertEqual(logMatchers.count, 1) + let logMatcher = logMatchers[0] + logMatcher.assertMessage(equals: "test message") + logMatcher.assertStatus(equals: "info") + } + + private func assertTracingDataWasCollected(by serverSession: ServerSession) throws { + let recordedRequests = try serverSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + try SpanMatcher.from(requests: requests).count == 1 + } + + assertTracing(requests: recordedRequests) + + let spanMatchers = try SpanMatcher.from(requests: recordedRequests) + XCTAssertEqual(spanMatchers.count, 1) + let spanMatcher = spanMatchers[0] + XCTAssertEqual(try spanMatcher.operationName(), "test span") + } + + private func assertRUMDataWasCollected(by serverSession: ServerSession) throws { + let recordedRequests = try serverSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in + // ignore telemetry and application start view events + let eventMatchers = try requests + .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } + .filterTelemetry() + .filterApplicationLaunchView() + + guard let session = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers).first else { + return false + } + + return session.views.count >= 3 + } + + assertRUM(requests: recordedRequests) + + // ignore telemetry and application start view events + let eventMatchers = try recordedRequests + .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } + .filterTelemetry() + .filterApplicationLaunchView() + + let session = try XCTUnwrap(RUMSessionMatcher.groupMatchersBySessions(eventMatchers).first) + sendCIAppLog(session) + + XCTAssertEqual(session.views[0].name, "Home") + XCTAssertGreaterThan(session.views[0].actionEvents.count, 0) + + XCTAssertEqual(session.views[1].name, "Picture") + XCTAssertEqual(session.views[1].resourceEvents.count, 1) + XCTAssertGreaterThan(session.views[1].actionEvents.count, 0) + + XCTAssertEqual(session.views[2].name, "Home") + } +} diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMCommonAsserts.swift b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMCommonAsserts.swift index 9e17831dad..90ad6cc24b 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMCommonAsserts.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/RUM/RUMCommonAsserts.swift @@ -56,7 +56,7 @@ extension RUMSessionMatcher { class func sessions(maxCount: Int, from requests: [HTTPServerMock.Request], eventsPatch: ((Data) throws -> Data)? = nil) throws -> [RUMSessionMatcher] { let eventMatchers = try requests .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody, eventsPatch: eventsPatch) } - .filter { event in try event.eventType() != "telemetry" } + .filterTelemetry() let sessionMatchers = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers).sorted(by: { return $0.views.first?.viewEvents.first?.date ?? 0 < $1.views.first?.viewEvents.first?.date ?? 0 }) diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj index 58f42e444e..69929664de 100644 --- a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -41,7 +41,7 @@ 6137E649252DD88D00720485 /* RUMModalViewsAutoInstrumentationScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6137E648252DD88D00720485 /* RUMModalViewsAutoInstrumentationScenario.storyboard */; }; 613B77382521E80800155458 /* RUMTabBarControllerScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613B77372521E80800155458 /* RUMTabBarControllerScenarioTests.swift */; }; 61410141251A454500E3C2D9 /* TracingCommonAsserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410140251A454500E3C2D9 /* TracingCommonAsserts.swift */; }; - 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; + 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* AppDelegate.swift */; }; 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3B24617013003D8BB8 /* IntegrationTests.swift */; }; 61441C4124617013003D8BB8 /* LoggingScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3C24617013003D8BB8 /* LoggingScenarioTests.swift */; }; @@ -110,6 +110,13 @@ D2791EF927170A760046E07A /* RUMSwiftUIScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */; }; D2F5BB36271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */; }; D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */; }; + D2FCA74D2B4D829F0014DC87 /* CSPictureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA7482B4D829F0014DC87 /* CSPictureViewController.swift */; }; + D2FCA74E2B4D829F0014DC87 /* StopCoreScenario.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2FCA7492B4D829F0014DC87 /* StopCoreScenario.storyboard */; }; + D2FCA74F2B4D829F0014DC87 /* CSHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA74A2B4D829F0014DC87 /* CSHomeViewController.swift */; }; + D2FCA7512B4D829F0014DC87 /* CoreScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA74C2B4D829F0014DC87 /* CoreScenarios.swift */; }; + D2FCA7532B4D84EE0014DC87 /* CustomURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA7522B4D84EE0014DC87 /* CustomURLSessionDelegate.swift */; }; + D2FCA7552B4D87110014DC87 /* StopCoreScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA7542B4D87110014DC87 /* StopCoreScenarioTests.swift */; }; + D2FCA7582B4DA8720014DC87 /* CSRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA7572B4DA8720014DC87 /* CSRootViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -172,7 +179,7 @@ 613B77372521E80800155458 /* RUMTabBarControllerScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTabBarControllerScenarioTests.swift; sourceTree = ""; }; 61410140251A454500E3C2D9 /* TracingCommonAsserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingCommonAsserts.swift; sourceTree = ""; }; 61441C0224616DE9003D8BB8 /* Integration Tests Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Integration Tests Runner.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppDelegate.swift; sourceTree = ""; }; + 61441C0424616DE9003D8BB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 61441C0D24616DEC003D8BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61441C2A24616F1D003D8BB8 /* IntegrationScenarios.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationScenarios.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61441C3B24617013003D8BB8 /* IntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; @@ -256,6 +263,13 @@ D2AEFF1B29925BEC00A28997 /* DatadogIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatadogIntegrationTests.xctestplan; sourceTree = ""; }; D2F5BB35271831C200BDE2A4 /* RUMSwiftUIInstrumentationScenario.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RUMSwiftUIInstrumentationScenario.storyboard; sourceTree = ""; }; D2F5BB372718331800BDE2A4 /* SwiftUIRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIRootViewController.swift; sourceTree = ""; }; + D2FCA7482B4D829F0014DC87 /* CSPictureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSPictureViewController.swift; sourceTree = ""; }; + D2FCA7492B4D829F0014DC87 /* StopCoreScenario.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = StopCoreScenario.storyboard; sourceTree = ""; }; + D2FCA74A2B4D829F0014DC87 /* CSHomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSHomeViewController.swift; sourceTree = ""; }; + D2FCA74C2B4D829F0014DC87 /* CoreScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreScenarios.swift; sourceTree = ""; }; + D2FCA7522B4D84EE0014DC87 /* CustomURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomURLSessionDelegate.swift; sourceTree = ""; }; + D2FCA7542B4D87110014DC87 /* StopCoreScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopCoreScenarioTests.swift; sourceTree = ""; }; + D2FCA7572B4DA8720014DC87 /* CSRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSRootViewController.swift; sourceTree = ""; }; E0F3607A83689002423C29E4 /* libPods-Runner iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E6057106F868E6C529F89BE9 /* Pods-Runner iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner iOS.release.xcconfig"; path = "Target Support Files/Pods-Runner iOS/Pods-Runner iOS.release.xcconfig"; sourceTree = ""; }; E93021D28790F380B3C63C4B /* libPods-Runner iOS-IntegrationScenarios.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner iOS-IntegrationScenarios.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -417,6 +431,7 @@ isa = PBXGroup; children = ( 614CADCD250FCA0200B93D2D /* TestScenarios.swift */, + D2FCA7462B4D829F0014DC87 /* Core */, 61337030250F829E00236D58 /* Logging */, 61337033250F847500236D58 /* Tracing */, 61337036250F84F100236D58 /* RUM */, @@ -488,7 +503,7 @@ 61441C0324616DE9003D8BB8 /* Runner */ = { isa = PBXGroup; children = ( - 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */, + 61441C0424616DE9003D8BB8 /* AppDelegate.swift */, 61441C9C2461A796003D8BB8 /* AppConfiguration.swift */, 614CADD62510BAC000B93D2D /* Environment.swift */, 6133702F250F829000236D58 /* Scenarios */, @@ -521,6 +536,7 @@ 6111544725C9A88B007C84C9 /* PersistenceHelper.swift */, 61098D2A27FEE3F00021237A /* MessagePortChannel.swift */, 61098D2E27FF16A20021237A /* RUMSessionEndView.swift */, + D2FCA7522B4D84EE0014DC87 /* CustomURLSessionDelegate.swift */, ); path = Utils; sourceTree = ""; @@ -607,6 +623,7 @@ 61F3CD9C251106EB00C816E5 /* Scenarios */ = { isa = PBXGroup; children = ( + D2FCA7562B4D87180014DC87 /* Core */, 61F3CD9D251106FA00C816E5 /* Logging */, 61F3CD9E251106FF00C816E5 /* Tracing */, 61F3CD9F2511070300C816E5 /* RUM */, @@ -768,6 +785,34 @@ path = xctestplans; sourceTree = ""; }; + D2FCA7462B4D829F0014DC87 /* Core */ = { + isa = PBXGroup; + children = ( + D2FCA74C2B4D829F0014DC87 /* CoreScenarios.swift */, + D2FCA7472B4D829F0014DC87 /* StopCoreInstance */, + ); + path = Core; + sourceTree = ""; + }; + D2FCA7472B4D829F0014DC87 /* StopCoreInstance */ = { + isa = PBXGroup; + children = ( + D2FCA7492B4D829F0014DC87 /* StopCoreScenario.storyboard */, + D2FCA7572B4DA8720014DC87 /* CSRootViewController.swift */, + D2FCA74A2B4D829F0014DC87 /* CSHomeViewController.swift */, + D2FCA7482B4D829F0014DC87 /* CSPictureViewController.swift */, + ); + path = StopCoreInstance; + sourceTree = ""; + }; + D2FCA7562B4D87180014DC87 /* Core */ = { + isa = PBXGroup; + children = ( + D2FCA7542B4D87110014DC87 /* StopCoreScenarioTests.swift */, + ); + path = Core; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -877,6 +922,7 @@ 9EC2835E26CFF57A00FACF1C /* RUMMobileVitalsScenario.storyboard in Resources */, 6167AD19251A27B80012B4D0 /* URLSessionScenario.storyboard in Resources */, 61F9CA792512593A000A5E61 /* RUMNavigationControllerScenario.storyboard in Resources */, + D2FCA74E2B4D829F0014DC87 /* StopCoreScenario.storyboard in Resources */, 61337035250F84BF00236D58 /* TracingManualInstrumentationScenario.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -942,6 +988,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D2FCA7532B4D84EE0014DC87 /* CustomURLSessionDelegate.swift in Sources */, D2F5BB382718331800BDE2A4 /* SwiftUIRootViewController.swift in Sources */, 618DCFE124C766F500589570 /* SendRUMFixture2ViewController.swift in Sources */, D22F06CF29DAE5360026CC3C /* KioskSendInterruptedEventsViewController.swift in Sources */, @@ -950,6 +997,7 @@ 6164AE89252B4ECA000D78C4 /* SendThirdPartyRequestsViewController.swift in Sources */, 612C13732A9DFE1C0086B5D1 /* SRMultipleViewsRecordingViewController.swift in Sources */, 611EA14225810E1900BC0E56 /* TSHomeViewController.swift in Sources */, + D2FCA7582B4DA8720014DC87 /* CSRootViewController.swift in Sources */, 612D8F6F25AEE6A7000E2E09 /* RUMScrubbingViewController.swift in Sources */, 61163C37252DDD60007DD5BF /* RUMMVSViewController.swift in Sources */, 61D50C5A2580EFF3006038A3 /* URLSessionScenarios.swift in Sources */, @@ -973,10 +1021,13 @@ 61D50C3C2580EEF8006038A3 /* LoggingScenarios.swift in Sources */, 611EA14E25810E3700BC0E56 /* TSConsentSettingViewController.swift in Sources */, 6164AF0E252C9016000D78C4 /* ObjcSendThirdPartyRequestsViewController.m in Sources */, - 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */, + D2FCA74D2B4D829F0014DC87 /* CSPictureViewController.swift in Sources */, + 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */, 6193DCCE251B6201009B8011 /* RUMTASScreen1ViewController.swift in Sources */, + D2FCA7512B4D829F0014DC87 /* CoreScenarios.swift in Sources */, 612C13702A9CF9140086B5D1 /* SRScenarios.swift in Sources */, 61098D2F27FF16A20021237A /* RUMSessionEndView.swift in Sources */, + D2FCA74F2B4D829F0014DC87 /* CSHomeViewController.swift in Sources */, 61163C3E252E0015007DD5BF /* RUMMVSModalViewController.swift in Sources */, 617247AF25DA9BEA007085B3 /* CrashReportingObjcHelpers.m in Sources */, 6193DCE1251B692C009B8011 /* RUMTASTableViewController.swift in Sources */, @@ -1024,6 +1075,7 @@ 612C13CC2A9F94AF0086B5D1 /* SRMultipleViewsRecordingScenarioTests.swift in Sources */, 61410141251A454500E3C2D9 /* TracingCommonAsserts.swift in Sources */, 615AAC29251E322D00C89EE9 /* RUMCommonAsserts.swift in Sources */, + D2FCA7552B4D87110014DC87 /* StopCoreScenarioTests.swift in Sources */, D2774EE7299E2E90004EC36A /* LogMatcher.swift in Sources */, 6164AF2E252C9C51000D78C4 /* RUMResourcesScenarioTests.swift in Sources */, D2774EE8299E2E90004EC36A /* JSONDataMatcher.swift in Sources */, diff --git a/IntegrationTests/Runner/AppConfiguration.swift b/IntegrationTests/Runner/AppConfiguration.swift index fdfa969036..e29b5c81f0 100644 --- a/IntegrationTests/Runner/AppConfiguration.swift +++ b/IntegrationTests/Runner/AppConfiguration.swift @@ -6,8 +6,15 @@ import UIKit import DatadogCore +import DatadogLogs +import DatadogTrace +import DatadogRUM import DatadogCrashReporting -import enum DatadogInternal.TrackingConsent + +@_exported import class DatadogInternal.DDURLSessionDelegate + +var logger: LoggerProtocol? +var rumMonitor: RUMMonitorProtocol { RUMMonitor.shared() } protocol AppConfiguration { /// The tracking consent value applied when initializing the SDK. @@ -67,3 +74,46 @@ struct UITestsAppConfiguration: AppConfiguration { return UIStoryboard(name: type(of: testScenario).storyboardName, bundle: nil) } } + +extension AppConfiguration { + func initializeSDK() { + // Initialize Datadog SDK + Datadog.initialize( + with: appConfiguration.sdkConfiguration(), + trackingConsent: appConfiguration.initialTrackingConsent + ) + + appConfiguration.testScenario?.configureFeatures() + + // Set user information + Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com", extraInfo: ["key-extraUserInfo": "value-extraUserInfo"]) + + // Create Logger + logger = Logger.create( + with: Logger.Configuration( + name: "logger-name", + networkInfoEnabled: true, + consoleLogFormat: .shortWith(prefix: "[iOS App] ") + ) + ) + + logger?.addAttribute(forKey: "device-model", value: UIDevice.current.model) + + #if DEBUG + logger?.addTag(withKey: "build_configuration", value: "debug") + #else + logger?.addTag(withKey: "build_configuration", value: "release") + #endif + + // Set highest verbosity level to see debugging logs from the SDK + Datadog.verbosityLevel = .debug + + // Enable RUM Views debugging + RUMMonitor.shared().debug = true + } + + func deinitializeSDK() { + Datadog.stopInstance() + logger = nil + } +} diff --git a/IntegrationTests/Runner/ExampleAppDelegate.swift b/IntegrationTests/Runner/AppDelegate.swift similarity index 53% rename from IntegrationTests/Runner/ExampleAppDelegate.swift rename to IntegrationTests/Runner/AppDelegate.swift index ac34ee1703..8da26219c2 100644 --- a/IntegrationTests/Runner/ExampleAppDelegate.swift +++ b/IntegrationTests/Runner/AppDelegate.swift @@ -10,18 +10,11 @@ import DatadogLogs import DatadogTrace import DatadogRUM -@_exported import enum DatadogInternal.TrackingConsent -@_exported import class DatadogInternal.DDURLSessionDelegate - -var logger: LoggerProtocol! -var tracer: OTTracer { Tracer.shared() } -var rumMonitor: RUMMonitorProtocol { RUMMonitor.shared() } - var serviceName = "integration-scenarios-service-name" var appConfiguration: AppConfiguration! @UIApplicationMain -class ExampleAppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -32,38 +25,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { appConfiguration = UITestsAppConfiguration() // Initialize Datadog SDK - Datadog.initialize( - with: appConfiguration.sdkConfiguration(), - trackingConsent: appConfiguration.initialTrackingConsent - ) - - // Set user information - Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com", extraInfo: ["key-extraUserInfo": "value-extraUserInfo"]) - - appConfiguration.testScenario?.configureFeatures() - - // Create Logger - logger = Logger.create( - with: Logger.Configuration( - name: "logger-name", - networkInfoEnabled: true, - consoleLogFormat: .shortWith(prefix: "[iOS App] ") - ) - ) - - logger.addAttribute(forKey: "device-model", value: UIDevice.current.model) - - #if DEBUG - logger.addTag(withKey: "build_configuration", value: "debug") - #else - logger.addTag(withKey: "build_configuration", value: "release") - #endif - - // Set highest verbosity level to see debugging logs from the SDK - Datadog.verbosityLevel = .debug - - // Enable RUM Views debugging - RUMMonitor.shared().debug = true + appConfiguration.initializeSDK() // Launch initial screen depending on the launch configuration if let storyboard = appConfiguration.initialStoryboard() { diff --git a/IntegrationTests/Runner/Scenarios/Core/CoreScenarios.swift b/IntegrationTests/Runner/Scenarios/Core/CoreScenarios.swift new file mode 100644 index 0000000000..86ebe07a78 --- /dev/null +++ b/IntegrationTests/Runner/Scenarios/Core/CoreScenarios.swift @@ -0,0 +1,59 @@ +/* + * 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 DatadogTrace +import DatadogRUM +import DatadogLogs +import DatadogCore + +internal class StopCoreScenario: TestScenario { + static let storyboardName = "StopCoreScenario" + + required init() { } + + func configureFeatures() { + // Enable RUM + var rumConfig = RUM.Configuration(applicationID: "rum-application-id") + rumConfig.customEndpoint = Environment.serverMockConfiguration()?.rumEndpoint + rumConfig.uiKitViewsPredicate = StopCoreScenarioUIKitRUMViewsPredicate() + rumConfig.uiKitActionsPredicate = DefaultUIKitRUMActionsPredicate() + rumConfig.urlSessionTracking = .init() + RUM.enable(with: rumConfig) + + // Enable Trace + var traceConfig = Trace.Configuration() + traceConfig.networkInfoEnabled = true + traceConfig.customEndpoint = Environment.serverMockConfiguration()?.tracesEndpoint + Trace.enable(with: traceConfig) + + // Enable Logs + Logs.enable( + with: Logs.Configuration( + customEndpoint: Environment.serverMockConfiguration()?.logsEndpoint + ) + ) + + URLSessionInstrumentation.enable( + with: URLSessionInstrumentation.Configuration(delegateClass: CustomURLSessionDelegate.self) + ) + + URLSessionInstrumentation.enable(with: .init(delegateClass: CustomURLSessionDelegate.self)) + } +} + +private struct StopCoreScenarioUIKitRUMViewsPredicate: UIKitRUMViewsPredicate { + func rumView(for viewController: UIViewController) -> RUMView? { + switch viewController { + case is CSHomeViewController: + return RUMView(name: "Home") + case is CSPictureViewController: + return RUMView(name: "Picture") + default: + return nil + } + } +} diff --git a/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSHomeViewController.swift b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSHomeViewController.swift new file mode 100644 index 0000000000..7b22f718f1 --- /dev/null +++ b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSHomeViewController.swift @@ -0,0 +1,22 @@ +/* + * 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 +import DatadogCore +import DatadogTrace + +internal class CSHomeViewController: UIViewController { + @IBAction func didTapTestLogging(_ sender: UIButton) { + sender.disableFor(seconds: 0.5) + logger?.info("test message") + } + + @IBAction func didTapTestTracing(_ sender: UIButton) { + sender.disableFor(seconds: 0.5) + let span = Tracer.shared().startSpan(operationName: "test span") + span.finish(at: Date(timeIntervalSinceNow: 1)) + } +} diff --git a/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSPictureViewController.swift b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSPictureViewController.swift new file mode 100644 index 0000000000..6aa25fba82 --- /dev/null +++ b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSPictureViewController.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 UIKit + +internal class CSPictureViewController: UIViewController { + let sessionDelegate = CustomURLSessionDelegate() + + private lazy var session = URLSession( + configuration: .default, + delegate: sessionDelegate, + delegateQueue: nil + ) + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var successLabel: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + successLabel.isHidden = true + } + + @IBAction func didTapDownloadImage(_ sender: UIButton) { + let enableSender = sender.disableUntilCompletion() + + let imageURL = URL(string: "https://imgix.datadoghq.com/img/about/presskit/usage/logousage_white.png")! + var imageRequest = URLRequest(url: imageURL) + imageRequest.cachePolicy = .reloadIgnoringLocalCacheData + + let imageTask = session.dataTask(with: imageRequest) { [weak self] data, _, error in + if let error = error { + // Crash the app, so we have obvious feedback in integration test + fatalError("Failed to download image: \(error)") + } else if let data = data { + DispatchQueue.main.async { + enableSender() + self?.showImage(from: data) + } + } else { + fatalError("Failed to download image with no clue") + } + } + imageTask.resume() + } + + private func showImage(from imageData: Data) { + imageView.image = UIImage(data: imageData) + successLabel.isHidden = false + } +} diff --git a/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSRootViewController.swift b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSRootViewController.swift new file mode 100644 index 0000000000..9c8d929c1d --- /dev/null +++ b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/CSRootViewController.swift @@ -0,0 +1,19 @@ +/* + * 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 +import DatadogCore +import DatadogTrace + +internal class CSRootViewController: UIViewController { + @IBAction func startCore(_ sender: UIButton) { + appConfiguration.initializeSDK() + } + + @IBAction func stopCore(_ sender: UIButton) { + appConfiguration.deinitializeSDK() + } +} diff --git a/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/StopCoreScenario.storyboard b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/StopCoreScenario.storyboard new file mode 100644 index 0000000000..ba17b2ed95 --- /dev/null +++ b/IntegrationTests/Runner/Scenarios/Core/StopCoreInstance/StopCoreScenario.storyboard @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntegrationTests/Runner/Scenarios/Logging/ManualInstrumentation/SendLogsFixtureViewController.swift b/IntegrationTests/Runner/Scenarios/Logging/ManualInstrumentation/SendLogsFixtureViewController.swift index 739aa25f1d..a5c215ff03 100644 --- a/IntegrationTests/Runner/Scenarios/Logging/ManualInstrumentation/SendLogsFixtureViewController.swift +++ b/IntegrationTests/Runner/Scenarios/Logging/ManualInstrumentation/SendLogsFixtureViewController.swift @@ -11,18 +11,18 @@ internal class SendLogsFixtureViewController: UIViewController { super.viewDidLoad() // Send logs - logger.addTag(withKey: "tag1", value: "tag-value") - logger.add(tag: "tag2") + logger?.addTag(withKey: "tag1", value: "tag-value") + logger?.add(tag: "tag2") - logger.addAttribute(forKey: "logger-attribute1", value: "string value") - logger.addAttribute(forKey: "logger-attribute2", value: 1_000) - logger.addAttribute(forKey: "some-url", value: URL(string: "https://example.com/image.png")!) + logger?.addAttribute(forKey: "logger-attribute1", value: "string value") + logger?.addAttribute(forKey: "logger-attribute2", value: 1_000) + logger?.addAttribute(forKey: "some-url", value: URL(string: "https://example.com/image.png")!) - logger.debug("debug message", attributes: ["attribute": "value"]) - logger.info("info message", attributes: ["attribute": "value"]) - logger.notice("notice message", attributes: ["attribute": "value"]) - logger.warn("warn message", attributes: ["attribute": "value"]) - logger.error("error message", attributes: ["attribute": "value"]) - logger.critical("critical message", attributes: ["attribute": "value"]) + logger?.debug("debug message", attributes: ["attribute": "value"]) + logger?.info("info message", attributes: ["attribute": "value"]) + logger?.notice("notice message", attributes: ["attribute": "value"]) + logger?.warn("warn message", attributes: ["attribute": "value"]) + logger?.error("error message", attributes: ["attribute": "value"]) + logger?.critical("critical message", attributes: ["attribute": "value"]) } } diff --git a/IntegrationTests/Runner/Scenarios/Tracing/ManualInstrumentation/SendTracesFixtureViewController.swift b/IntegrationTests/Runner/Scenarios/Tracing/ManualInstrumentation/SendTracesFixtureViewController.swift index c0d5861d6a..5ebf25b1d2 100644 --- a/IntegrationTests/Runner/Scenarios/Tracing/ManualInstrumentation/SendTracesFixtureViewController.swift +++ b/IntegrationTests/Runner/Scenarios/Tracing/ManualInstrumentation/SendTracesFixtureViewController.swift @@ -14,6 +14,8 @@ internal class SendTracesFixtureViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + let tracer = Tracer.shared() + let viewLoadingSpan = tracer .startRootSpan(operationName: "view loading") .setActive() diff --git a/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSHomeViewController.swift b/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSHomeViewController.swift index 34e699e4f1..570666c619 100644 --- a/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSHomeViewController.swift +++ b/IntegrationTests/Runner/Scenarios/TrackingConsent/TrackingConsent/TSHomeViewController.swift @@ -6,6 +6,7 @@ import UIKit import DatadogCore +import DatadogTrace internal class TSHomeViewController: UIViewController { override func viewDidLoad() { @@ -39,13 +40,12 @@ internal class TSHomeViewController: UIViewController { @IBAction func didTapTestLogging(_ sender: UIButton) { sender.disableFor(seconds: 0.5) - logger.info("test message") + logger?.info("test message") } @IBAction func didTapTestTracing(_ sender: UIButton) { sender.disableFor(seconds: 0.5) - - let span = tracer.startSpan(operationName: "test span") + let span = Tracer.shared().startSpan(operationName: "test span") span.finish(at: Date(timeIntervalSinceNow: 1)) } } diff --git a/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift b/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift index 637b7ffa6f..f9e5efd261 100644 --- a/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift +++ b/IntegrationTests/Runner/Scenarios/URLSession/URLSessionScenarios.swift @@ -39,11 +39,6 @@ private class CompositedURLSessionDelegate: NSObject, URLSessionTaskDelegate, UR } } -/// An example of instrumenting existing `URLSessionDelegate` with `DDURLSessionDelegate` through inheritance. -private class CustomURLSessionDelegate: NSObject, URLSessionDataDelegate { - -} - /// Base scenario for `URLSession` and `NSURLSession` instrumentation. It makes /// both Swift and Objective-C tests share the same endpoints and SDK configuration. /// diff --git a/IntegrationTests/Runner/Utils/CustomURLSessionDelegate.swift b/IntegrationTests/Runner/Utils/CustomURLSessionDelegate.swift new file mode 100644 index 0000000000..c25d162552 --- /dev/null +++ b/IntegrationTests/Runner/Utils/CustomURLSessionDelegate.swift @@ -0,0 +1,12 @@ +/* + * 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 + +/// A custion ``URLSessionDataDelegate`` for instrumenting ``URLSession``. +internal class CustomURLSessionDelegate: NSObject, URLSessionDataDelegate { + +} diff --git a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift index 77b971786e..87b1ec631f 100644 --- a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift +++ b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift @@ -64,6 +64,12 @@ public class ServerMock { return ServerSession(server: self) } + public func clearAllRequests() { + var request = URLRequest(url: baseURL.appendingPathComponent("/requests")) + request.httpMethod = "DELETE" + URLSession.shared.dataTask(with: request).resume() + } + // MARK: - Endpoints /// Fetches all requests recorded by the server. diff --git a/instrumented-tests/http-server-mock/python/start_mock_server.py b/instrumented-tests/http-server-mock/python/start_mock_server.py index de22cddb38..d62c67925f 100755 --- a/instrumented-tests/http-server-mock/python/start_mock_server.py +++ b/instrumented-tests/http-server-mock/python/start_mock_server.py @@ -47,6 +47,14 @@ def do_GET(self): (r"/inspect$", self.__GET_inspect), ]) + def do_DELETE(self): + """ + Routes all incoming DELETE requests + """ + self.__route([ + (r"/requests$", self.__DELETE_requests), + ]) + def __POST_any(self, parameters): """ POST /* @@ -84,6 +92,16 @@ def __GET_inspect(self, parameters): return json.dumps(inspection_info).encode("utf-8") + def __DELETE_requests(self, parameters): + """ + DELETE /requests + + Remove all. + """ + global history + history.clear() + return bytes() + def __route(self, routes): try: for url_regexp, method in routes: @@ -132,6 +150,9 @@ def all_requests(self): def request(self, request_id): return self.__requests[int(request_id)] + def clear(self): + self.__requests.clear() + # If any previous instance of this server is running - kill it os.system('pkill -f start_mock_server.py') time.sleep(1) # wait a bit until socket is eventually released From 07a9f23258c35b78ec63db738302cf141a790c8b Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Tue, 9 Jan 2024 19:32:53 +0100 Subject: [PATCH 25/60] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4820e0cd..65c3dc15ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - [FIX] RUM session not being linked to spans. See [#1615][] +- [FEATURE] Allow stopping a core instance. See [#1541][] # 2.6.0 / 09-01-2024 - [FEATURE] Add `currentSessionID(completion:)` accessor to access the current session ID. @@ -575,6 +576,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609 [#1615]: https://github.com/DataDog/dd-sdk-ios/pull/1615 [#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 +[#1541]: https://github.com/DataDog/dd-sdk-ios/pull/1541 [#1596]: https://github.com/DataDog/dd-sdk-ios/pull/1596 [#1597]: https://github.com/DataDog/dd-sdk-ios/pull/1597 [@00fa9a]: https://github.com/00FA9A From 3324a70a439209849aa4ed4e87f240fdbc4b48d7 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Wed, 10 Jan 2024 14:18:16 +0100 Subject: [PATCH 26/60] Update documentation --- DatadogCore/Sources/Datadog.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 1361bff8f9..8a3f21d4c1 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -302,8 +302,11 @@ public enum Datadog { } /// Stops the initialized SDK instance attached to the given name. + /// + /// Stopping a core instance will stop all current processes by deallocating all Features registered + /// in the core as well as their storage & upload units. /// - /// - Parameter instanceName: The core instance name + /// - Parameter instanceName: the name of the instance to stop. public static func stopInstance(named instanceName: String = CoreRegistry.defaultInstanceName) { let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore core?.stop() From 94180ad18a9c2267761855a094678b31e735cbad Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 11 Jan 2024 17:27:44 +0100 Subject: [PATCH 27/60] Fix retain cycle in context provider --- DatadogCore/Sources/Core/Context/DatadogContextProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift index 4ac408377d..b328816b44 100644 --- a/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift +++ b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift @@ -125,8 +125,8 @@ internal final class DatadogContextProvider { /// - keyPath: A context's key path that supports reading from and writing to the resulting value. /// - publisher: The context value publisher. func subscribe(_ keyPath: WritableKeyPath, to publisher: Publisher) where Publisher: ContextValuePublisher { - let subscription = publisher.subscribe { value in - self.write { $0[keyPath: keyPath] = value } + let subscription = publisher.subscribe { [weak self] value in + self?.write { $0[keyPath: keyPath] = value } } write { From 8f4a0dc6be30b007d98d0536b58e0ea1deccfd62 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 11 Jan 2024 17:48:12 +0100 Subject: [PATCH 28/60] Fix leak in `VitalRefreshRateReader` --- .../RUMVitals/VitalRefreshRateReader.swift | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift index f9924f3d97..ea44a787cb 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift @@ -9,14 +9,17 @@ import UIKit /// A class reading the refresh rate (frames per second) of the main screen internal class VitalRefreshRateReader: ContinuousVitalReader { - private var valuePublishers = [VitalPublisher]() + private static var backendSupportedFrameRate = 60.0 + private var valuePublishers: [VitalPublisher] = [] private var displayLink: CADisplayLink? private var lastFrameTimestamp: CFTimeInterval? private var nextFrameDuration: CFTimeInterval? - private static var backendSupportedFrameRate = 60.0 + private let notificationCenter: NotificationCenter init(notificationCenter: NotificationCenter = .default) { + self.notificationCenter = notificationCenter + notificationCenter.addObserver( self, selector: #selector(appWillResignActive), @@ -35,6 +38,7 @@ internal class VitalRefreshRateReader: ContinuousVitalReader { deinit { stop() + notificationCenter.removeObserver(self) } /// `VitalRefreshRateReader` keeps pushing data to its `observers` at every new frame. @@ -87,15 +91,28 @@ internal class VitalRefreshRateReader: ContinuousVitalReader { // MARK: - Private - @objc - private func displayTick(link: CADisplayLink) { - guard let fps = framesPerSecond(provider: link) else { - return + // Holds a weak ref to the read and prevent retain cycle: + // reader -> displayLink -> reader + private class CADisplayLinker { + weak var reader: VitalRefreshRateReader? + + init(reader: VitalRefreshRateReader) { + self.reader = reader } - for publisher in valuePublishers { - publisher.mutateAsync { currentInfo in - currentInfo.addSample(fps) + @objc + func displayTick(link: CADisplayLink) { + guard + let reader = reader, + let fps = reader.framesPerSecond(provider: link) + else { + return + } + + for publisher in reader.valuePublishers { + publisher.mutateAsync { currentInfo in + currentInfo.addSample(fps) + } } } } @@ -105,7 +122,10 @@ internal class VitalRefreshRateReader: ContinuousVitalReader { return } - displayLink = CADisplayLink(target: self, selector: #selector(displayTick(link:))) + displayLink = CADisplayLink( + target: CADisplayLinker(reader: self), + selector: #selector(CADisplayLinker.displayTick(link:)) + ) // NOTE: RUMM-1544 `.default` mode doesn't get fired while scrolling the UI, `.common` does. displayLink?.add(to: .main, forMode: .common) } From 447d2a0f0564512833a9d33397c2a88f1506709b Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 11 Jan 2024 18:46:31 +0100 Subject: [PATCH 29/60] Update StopCoreScenarioTests.swift --- .../Core/StopCoreScenarioTests.swift | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift b/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift index 6a37c55f3a..a52b5e956e 100644 --- a/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift +++ b/IntegrationTests/IntegrationScenarios/Scenarios/Core/StopCoreScenarioTests.swift @@ -73,15 +73,15 @@ class StopCoreScenarioTests: IntegrationTests, LoggingCommonAsserts, TracingComm ) ) - // Play scenarios - + // Play scenarios after first init var root = playScenario() try assertLoggingDataWasCollected(by: loggingServerSession) try assertTracingDataWasCollected(by: tracingServerSession) - try assertRUMDataWasCollected(by: rumServerSession) + try assertFirstRUMSessionWasCollected(by: rumServerSession) server.clearAllRequests() + // Stop the core and replay the scenario root.stopCore() root = playScenario(from: root) @@ -94,12 +94,13 @@ class StopCoreScenarioTests: IntegrationTests, LoggingCommonAsserts, TracingComm let recordedRUMRequests = try rumServerSession.getRecordedRequests() XCTAssertEqual(recordedRUMRequests.count, 0, "No RUM data should be send.") + // Restart the core and replay the scenario root.startCore() root = playScenario(from: root) try assertLoggingDataWasCollected(by: loggingServerSession) try assertTracingDataWasCollected(by: tracingServerSession) - try assertRUMDataWasCollected(by: rumServerSession) + try assertFirstRUMSessionWasCollected(by: rumServerSession) } /// Plays following scenario for started application: @@ -147,39 +148,27 @@ class StopCoreScenarioTests: IntegrationTests, LoggingCommonAsserts, TracingComm XCTAssertEqual(try spanMatcher.operationName(), "test span") } - private func assertRUMDataWasCollected(by serverSession: ServerSession) throws { + private func assertFirstRUMSessionWasCollected(by serverSession: ServerSession) throws { + // Get RUM Sessions with expected number of View visits let recordedRequests = try serverSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in - // ignore telemetry and application start view events - let eventMatchers = try requests - .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } - .filterTelemetry() - .filterApplicationLaunchView() - - guard let session = try RUMSessionMatcher.groupMatchersBySessions(eventMatchers).first else { - return false - } - - return session.views.count >= 3 + try RUMSessionMatcher.singleSession(from: requests)?.views.count == 4 } assertRUM(requests: recordedRequests) - // ignore telemetry and application start view events - let eventMatchers = try recordedRequests - .flatMap { request in try RUMEventMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } - .filterTelemetry() - .filterApplicationLaunchView() - - let session = try XCTUnwrap(RUMSessionMatcher.groupMatchersBySessions(eventMatchers).first) + let session = try XCTUnwrap(RUMSessionMatcher.singleSession(from: recordedRequests)) sendCIAppLog(session) - XCTAssertEqual(session.views[0].name, "Home") - XCTAssertGreaterThan(session.views[0].actionEvents.count, 0) + XCTAssertTrue(session.views[0].isApplicationLaunchView()) + XCTAssertEqual(session.views[0].actionEvents[0].action.type, .applicationStart) - XCTAssertEqual(session.views[1].name, "Picture") - XCTAssertEqual(session.views[1].resourceEvents.count, 1) + XCTAssertEqual(session.views[1].name, "Home") XCTAssertGreaterThan(session.views[1].actionEvents.count, 0) - XCTAssertEqual(session.views[2].name, "Home") + XCTAssertEqual(session.views[2].name, "Picture") + XCTAssertEqual(session.views[2].resourceEvents.count, 1) + XCTAssertGreaterThan(session.views[2].actionEvents.count, 0) + + XCTAssertEqual(session.views[3].name, "Home") } } From 6066c1079b76135772114176b9abe1290598ddf3 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 11 Jan 2024 19:23:10 +0100 Subject: [PATCH 30/60] Update DatadogCoreTests.swift --- .../Tests/Datadog/DatadogCore/DatadogCoreTests.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift index 021f9f91aa..c718730dcd 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -311,10 +311,6 @@ class DatadogCoreTests: XCTestCase { let scope = try XCTUnwrap(core.scope(for: FeatureMock.name)) // When - scope.eventWriteContext { context, writer in - writer.write(value: FeatureMock.Event(event: "should not be sent")) - } - core.stop() scope.eventWriteContext { context, writer in @@ -323,7 +319,7 @@ class DatadogCoreTests: XCTestCase { // Then XCTAssertNil(core.scope(for: FeatureMock.name)) - core.flushAndTearDown() + core.flush() XCTAssertEqual(requestBuilderSpy.requestParameters.count, 0, "It should not send any request") } } From deaea10c5d55b7ea1d6555580ca71dec117c1d71 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 6 Dec 2023 11:00:17 +0000 Subject: [PATCH 31/60] Add OSLog support for debug logging --- DatadogCore/Sources/Datadog.swift | 6 ++--- .../Sources/CrashReporting.swift | 2 +- .../PLCrashReporterPlugin.swift | 4 ++-- DatadogInternal/Sources/DD.swift | 22 ++++++++++++++++++- .../HostsSanitizer.swift | 3 ++- .../URLSessionInstrumentation.swift | 4 ++-- .../Sources/Telemetry/InternalLogger.swift | 13 +++++------ DatadogLogs/Sources/ConsoleLogger.swift | 7 +++--- DatadogLogs/Sources/LoggerProtocol.swift | 9 ++++++++ DatadogLogs/Sources/Logs.swift | 2 +- .../Instrumentation/RUMInstrumentation.swift | 6 +++-- DatadogRUM/Sources/RUM.swift | 4 ++-- DatadogRUM/Sources/RUMMonitor.swift | 2 +- .../Sources/SessionReplay.swift | 2 +- DatadogTrace/Sources/Trace.swift | 2 +- DatadogTrace/Sources/Tracer.swift | 2 +- 16 files changed, 60 insertions(+), 30 deletions(-) diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 8a3f21d4c1..b03f11189e 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -363,7 +363,7 @@ public enum Datadog { ) -> DatadogCoreProtocol { // TODO: RUMM-511 remove this warning #if targetEnvironment(macCatalyst) - consolePrint("⚠️ Catalyst is not officially supported by Datadog SDK: some features may NOT be functional!") + consolePrint("⚠️ Catalyst is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) #endif do { @@ -373,7 +373,7 @@ public enum Datadog { instanceName: instanceName ) } catch { - consolePrint("\(error)") + consolePrint("\(error)", .error) return NOPDatadogCore() } } @@ -389,7 +389,7 @@ public enum Datadog { let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) if debug { - consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument") + consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) Datadog.verbosityLevel = .debug } diff --git a/DatadogCrashReporting/Sources/CrashReporting.swift b/DatadogCrashReporting/Sources/CrashReporting.swift index f506f4b139..5a7e8caee4 100644 --- a/DatadogCrashReporting/Sources/CrashReporting.swift +++ b/DatadogCrashReporting/Sources/CrashReporting.swift @@ -39,7 +39,7 @@ public final class CrashReporting { core.telemetry .configuration(trackErrors: true) } catch { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } } diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift index 8b91112091..6d4f93a903 100644 --- a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift @@ -28,7 +28,7 @@ internal class PLCrashReporterPlugin: NSObject, CrashReportingPlugin { do { thirdPartyCrashReporter = try thirdPartyCrashReporterFactory() } catch { - consolePrint("🔥 DatadogCrashReporting error: failed to enable crash reporter: \(error)") + consolePrint("🔥 DatadogCrashReporting error: failed to enable crash reporter: \(error)", .error) } } } @@ -51,7 +51,7 @@ internal class PLCrashReporterPlugin: NSObject, CrashReportingPlugin { } } catch { _ = completion(nil) - consolePrint("🔥 DatadogCrashReporting error: failed to load crash report: \(error)") + consolePrint("🔥 DatadogCrashReporting error: failed to load crash report: \(error)", .error) } } diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index 362f5b970b..ad62926dcd 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -25,5 +25,25 @@ public struct DD { ) } +import OSLog + /// Function printing `String` content to console. -public var consolePrint: (String) -> Void = { print($0) } +public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in + if #available(iOS 14.0, *) { + switch level { + case .debug: Logger.datadog.debug("\(message)") + case .warn: Logger.datadog.warning("\(message)") + case .error: Logger.datadog.critical("\(message)") + case .critical: Logger.datadog.fault("\(message)") + } + } else { + print(message) + } +} + +#if canImport(OSLog) +@available(iOS 14.0, *) +extension Logger { + static let datadog = Logger(subsystem: "dd-sdk-ios", category: "insights") +} +#endif diff --git a/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift b/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift index 8ca6bef4b9..ba2e50e30c 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift @@ -53,7 +53,8 @@ public struct HostsSanitizer: HostsSanitizing { consolePrint( """ ⚠️ \(warningMessage): \(warning) - """ + """, + .warn ) } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift index bb3f98cd91..faa5e86e02 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift @@ -17,7 +17,7 @@ public enum URLSessionInstrumentation { do { try enableOrThrow(with: configuration, in: core) } catch let error { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } @@ -37,7 +37,7 @@ public enum URLSessionInstrumentation { do { try disableOrThrow(delegateClass: delegateClass, in: core) } catch let error { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } diff --git a/DatadogInternal/Sources/Telemetry/InternalLogger.swift b/DatadogInternal/Sources/Telemetry/InternalLogger.swift index 2481407494..10d2220a8b 100644 --- a/DatadogInternal/Sources/Telemetry/InternalLogger.swift +++ b/DatadogInternal/Sources/Telemetry/InternalLogger.swift @@ -16,14 +16,14 @@ public struct InternalLogger: CoreLogger { /// Formatter used to format the time accordingly for local device. private let dateFormatter: DateFormatterType /// The print function. - private let printFunction: (String) -> Void + private let printFunction: (String, CoreLoggerLevel) -> Void /// V1's verbosity level. Only logs above or equal to this level wil be printed. private let currentVerbosityLevel: () -> CoreLoggerLevel? public init( dateProvider: DateProvider, timeZone: TimeZone, - printFunction: @escaping (String) -> Void, + printFunction: @escaping (String, CoreLoggerLevel) -> Void, verbosityLevel: @escaping () -> CoreLoggerLevel? ) { self.dateProvider = dateProvider @@ -39,19 +39,18 @@ public struct InternalLogger: CoreLogger { return // if no `Datadog.verbosityLevel` is set or it is set above this level } - print(message: message(), error: error, emoji: level.emojiPrefix) + print(message: message(), error: error, level: level) } // MARK: - Private - private func print(message: @autoclosure () -> String, error: Error?, emoji: String) { - var log = buildMessageString(message: message(), emoji: emoji) + private func print(message: @autoclosure () -> String, error: Error?, level: CoreLoggerLevel) { + var log = buildMessageString(message: message(), emoji: level.emojiPrefix) if let error = error { log += "\n\nError details:\n\(buildErrorString(error: error))" } - - printFunction(log) + printFunction(log, level) } private func buildMessageString(message: @autoclosure () -> String, emoji: String) -> String { diff --git a/DatadogLogs/Sources/ConsoleLogger.swift b/DatadogLogs/Sources/ConsoleLogger.swift index 062dd2ca3b..73f26b0929 100644 --- a/DatadogLogs/Sources/ConsoleLogger.swift +++ b/DatadogLogs/Sources/ConsoleLogger.swift @@ -23,12 +23,12 @@ internal final class ConsoleLogger: LoggerProtocol { /// The prefix to use when rendering log. private let prefix: String /// The function used to render log. - private let printFunction: (String) -> Void + private let printFunction: (String, CoreLoggerLevel) -> Void init( configuration: Configuration, dateProvider: DateProvider, - printFunction: @escaping (String) -> Void + printFunction: @escaping (String, CoreLoggerLevel) -> Void ) { self.dateProvider = dateProvider self.timeFormatter = presentationDateFormatter(withTimeZone: configuration.timeZone) @@ -64,8 +64,7 @@ internal final class ConsoleLogger: LoggerProtocol { if let errorString = errorString { log += "\n\nError details:\n\(errorString)" } - - printFunction(log) + printFunction(log, level.asCoreLoggerLevel()) } private func buildErrorString(error: DDError) -> String { diff --git a/DatadogLogs/Sources/LoggerProtocol.swift b/DatadogLogs/Sources/LoggerProtocol.swift index 5ed1d2a161..423f611851 100644 --- a/DatadogLogs/Sources/LoggerProtocol.swift +++ b/DatadogLogs/Sources/LoggerProtocol.swift @@ -16,6 +16,15 @@ public enum LogLevel: Int, Codable { case warn case error case critical + + func asCoreLoggerLevel() -> CoreLoggerLevel { + switch self { + case .debug, .info, .notice: return .debug + case .warn: return .warn + case .error: return .error + case .critical: return .critical + } + } } /// Datadog Logger. diff --git a/DatadogLogs/Sources/Logs.swift b/DatadogLogs/Sources/Logs.swift index 1127714422..5025a16dc2 100644 --- a/DatadogLogs/Sources/Logs.swift +++ b/DatadogLogs/Sources/Logs.swift @@ -70,7 +70,7 @@ public enum Logs { do { try core.register(feature: feature) } catch { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } } diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 47c6826ddb..22a322a8ed 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -53,7 +53,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { } } catch { consolePrint( - "🔥 Datadog SDK error: UIKit RUM Views tracking can't be enabled due to error: \(error)" + "🔥 Datadog SDK error: UIKit RUM Views tracking can't be enabled due to error: \(error)", + .error ) } @@ -65,7 +66,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { } } catch { consolePrint( - "🔥 Datadog SDK error: RUM Actions tracking can't be enabled due to error: \(error)" + "🔥 Datadog SDK error: RUM Actions tracking can't be enabled due to error: \(error)", + .error ) } diff --git a/DatadogRUM/Sources/RUM.swift b/DatadogRUM/Sources/RUM.swift index 106d2cfce3..6da9385ecd 100644 --- a/DatadogRUM/Sources/RUM.swift +++ b/DatadogRUM/Sources/RUM.swift @@ -22,7 +22,7 @@ public enum RUM { do { try enableOrThrow(with: configuration, in: core) } catch let error { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } @@ -45,7 +45,7 @@ public enum RUM { } if configuration.debugViews { - consolePrint("⚠️ Overriding RUM debugging with DD_DEBUG_RUM launch argument") + consolePrint("⚠️ Overriding RUM debugging with DD_DEBUG_RUM launch argument", .warn) rum.monitor.debug = true } diff --git a/DatadogRUM/Sources/RUMMonitor.swift b/DatadogRUM/Sources/RUMMonitor.swift index 05662f95c6..cad407c31c 100644 --- a/DatadogRUM/Sources/RUMMonitor.swift +++ b/DatadogRUM/Sources/RUMMonitor.swift @@ -40,7 +40,7 @@ public class RUMMonitor { return feature.monitor } catch { - consolePrint("\(error)") + consolePrint("\(error)", .error) return NOPMonitor() } } diff --git a/DatadogSessionReplay/Sources/SessionReplay.swift b/DatadogSessionReplay/Sources/SessionReplay.swift index 8eab5c2189..538d53738e 100644 --- a/DatadogSessionReplay/Sources/SessionReplay.swift +++ b/DatadogSessionReplay/Sources/SessionReplay.swift @@ -25,7 +25,7 @@ public enum SessionReplay { do { try enableOrThrow(with: configuration, in: core) } catch let error { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } diff --git a/DatadogTrace/Sources/Trace.swift b/DatadogTrace/Sources/Trace.swift index 81b789d4c6..a6ea667938 100644 --- a/DatadogTrace/Sources/Trace.swift +++ b/DatadogTrace/Sources/Trace.swift @@ -22,7 +22,7 @@ public enum Trace { do { try enableOrThrow(with: configuration, in: core) } catch let error { - consolePrint("\(error)") + consolePrint("\(error)", .error) } } diff --git a/DatadogTrace/Sources/Tracer.swift b/DatadogTrace/Sources/Tracer.swift index 71c798d86d..2afaf83036 100644 --- a/DatadogTrace/Sources/Tracer.swift +++ b/DatadogTrace/Sources/Tracer.swift @@ -73,7 +73,7 @@ public class Tracer { return feature.tracer } catch { - consolePrint("\(error)") + consolePrint("\(error)", .error) return DDNoopTracer() } } From fe40b0e920c5dd4eaea6a8a69b78c2bc3fd3da36 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 8 Jan 2024 15:32:43 +0000 Subject: [PATCH 32/60] Set explicit privacy --- DatadogInternal/Sources/DD.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index ad62926dcd..5a9c77fcc0 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -31,10 +31,10 @@ import OSLog public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in if #available(iOS 14.0, *) { switch level { - case .debug: Logger.datadog.debug("\(message)") - case .warn: Logger.datadog.warning("\(message)") - case .error: Logger.datadog.critical("\(message)") - case .critical: Logger.datadog.fault("\(message)") + case .debug: Logger.datadog.debug("\(message, privacy: .private)") + case .warn: Logger.datadog.warning("\(message, privacy: .private)") + case .error: Logger.datadog.critical("\(message, privacy: .private)") + case .critical: Logger.datadog.fault("\(message, privacy: .private)") } } else { print(message) From 949050d634f4a7ee3d071cdd97d48569e7d704c6 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 8 Jan 2024 16:07:42 +0000 Subject: [PATCH 33/60] Update tests --- .../Tests/Datadog/Core/DD/InternalLoggerTests.swift | 8 ++++---- DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift | 2 +- DatadogCore/Tests/Datadog/DatadogTests.swift | 2 +- DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift | 4 ++-- DatadogCore/Tests/Datadog/TracerTests.swift | 4 ++-- .../NetworkInstrumentation/HostsSanitizerTests.swift | 2 +- DatadogRUM/Tests/RUMTests.swift | 2 +- DatadogTrace/Tests/TraceTests.swift | 2 +- TestUtilities/Mocks/PrintFunctionMock.swift | 3 ++- 9 files changed, 15 insertions(+), 14 deletions(-) diff --git a/DatadogCore/Tests/Datadog/Core/DD/InternalLoggerTests.swift b/DatadogCore/Tests/Datadog/Core/DD/InternalLoggerTests.swift index 7145af782d..b1ca7d17b0 100644 --- a/DatadogCore/Tests/Datadog/Core/DD/InternalLoggerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/DD/InternalLoggerTests.swift @@ -19,7 +19,7 @@ class InternalLoggerTests: XCTestCase { using: .mockDecember15th2019At10AMUTC(addingTimeInterval: 4.2) ), timeZone: .UTC, - printFunction: mock.print(message:), + printFunction: mock.print(message:level:), verbosityLevel: { .debug } ) @@ -44,7 +44,7 @@ class InternalLoggerTests: XCTestCase { using: .mockDecember15th2019At10AMUTC() ), timeZone: .UTC, - printFunction: mock.print(message:), + printFunction: mock.print(message:level:), verbosityLevel: { .debug } ) @@ -86,7 +86,7 @@ class InternalLoggerTests: XCTestCase { using: .mockDecember15th2019At10AMUTC() ), timeZone: .UTC, - printFunction: mock.print(message:), + printFunction: mock.print(message:level:), verbosityLevel: { verbosityLevel } ) @@ -130,7 +130,7 @@ class InternalLoggerTests: XCTestCase { let logger = InternalLogger( dateProvider: SystemDateProvider(), timeZone: .UTC, - printFunction: mock.print(message:), + printFunction: mock.print(message:level:), verbosityLevel: { verbosityLevel } ) diff --git a/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift b/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift index 173288f1ba..bfe7096583 100644 --- a/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift @@ -23,7 +23,7 @@ class DatadogConfigurationTests: XCTestCase { } override func tearDown() { - consolePrint = { print($0) } + consolePrint = { message, _ in print(message) } printFunction = nil XCTAssertFalse(Datadog.isInitialized()) super.tearDown() diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index 84dd88daf5..ea194d4c4f 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -25,7 +25,7 @@ class DatadogTests: XCTestCase { } override func tearDown() { - consolePrint = { print($0) } + consolePrint = { message, _ in print(message) } printFunction = nil XCTAssertFalse(Datadog.isInitialized()) super.tearDown() diff --git a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift index 6582b8cef7..9dcf403544 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift @@ -1230,7 +1230,7 @@ class RUMMonitorTests: XCTestCase { func testGivenSDKNotInitialized_whenObtainingSharedMonitor_itPrintsError() throws { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // Given let core = NOPDatadogCore() @@ -1249,7 +1249,7 @@ class RUMMonitorTests: XCTestCase { func testGivenRUMNotEnabled_whenObtainingSharedMonitor_itPrintsError() throws { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // Given let core = FeatureRegistrationCoreMock() diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index 0d265975c2..3442fc99b3 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -1138,7 +1138,7 @@ class TracerTests: XCTestCase { func testGivenSDKNotInitialized_whenObtainingSharedTracer_itPrintsError() { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // given let core = NOPDatadogCore() @@ -1158,7 +1158,7 @@ class TracerTests: XCTestCase { func testGivenTraceNotEnabled_whenObtainingSharedTracer_itPrintsError() { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // given let core = FeatureRegistrationCoreMock() diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift index 46f68e3fb1..1eac303083 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift @@ -12,7 +12,7 @@ class HostsSanitizerTests: XCTestCase { func testSanitizationAndWarningMessages() throws { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // When let hosts: Set = [ diff --git a/DatadogRUM/Tests/RUMTests.swift b/DatadogRUM/Tests/RUMTests.swift index 12a0f6a6be..0b7efa1364 100644 --- a/DatadogRUM/Tests/RUMTests.swift +++ b/DatadogRUM/Tests/RUMTests.swift @@ -44,7 +44,7 @@ class RUMTests: XCTestCase { func testWhenEnabledInNOPCore_itPrintsError() { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // When RUM.enable(with: config, in: NOPDatadogCore()) diff --git a/DatadogTrace/Tests/TraceTests.swift b/DatadogTrace/Tests/TraceTests.swift index 0aab78ef1f..e49883aa83 100644 --- a/DatadogTrace/Tests/TraceTests.swift +++ b/DatadogTrace/Tests/TraceTests.swift @@ -35,7 +35,7 @@ class TraceTests: XCTestCase { func testWhenEnabledInNOPCore_itPrintsError() { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // When Trace.enable(with: config, in: NOPDatadogCore()) diff --git a/TestUtilities/Mocks/PrintFunctionMock.swift b/TestUtilities/Mocks/PrintFunctionMock.swift index b7db949b1f..0935315c5a 100644 --- a/TestUtilities/Mocks/PrintFunctionMock.swift +++ b/TestUtilities/Mocks/PrintFunctionMock.swift @@ -5,6 +5,7 @@ */ import Foundation +import DatadogInternal // MARK: - Global Dependencies Mocks @@ -21,7 +22,7 @@ public class PrintFunctionMock { public init() { } - public func print(message: String) { + public func print(message: String, level: CoreLoggerLevel) { printedMessages.append(message) } From 527ccc507daa4c196740b898ef11285f51757755 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Mon, 8 Jan 2024 16:49:39 +0000 Subject: [PATCH 34/60] Fix tests --- .../Tests/CrashReportingPluginTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift index 7da1a3013c..f4dbe1e01e 100644 --- a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift +++ b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift @@ -104,8 +104,8 @@ class CrashReportingPluginTests: XCTestCase { let expectation = self.expectation(description: "No Crash Report was delivered to the caller.") var errorPrinted: String? - consolePrint = { errorPrinted = $0 } - defer { consolePrint = { print($0) } } + consolePrint = { message, _ in errorPrinted = message } + defer { consolePrint = { message, _ in print(message) } } let crashReporter = try ThirdPartyCrashReporterMock() let plugin = PLCrashReporterPlugin { crashReporter } @@ -134,8 +134,8 @@ class CrashReportingPluginTests: XCTestCase { func testWhenCrashReporterCannotBeEnabled_itPrintsError() { var errorPrinted: String? - consolePrint = { errorPrinted = $0 } - defer { consolePrint = { print($0) } } + consolePrint = { message, _ in errorPrinted = message } + defer { consolePrint = { message, _ in print(message) } } // When ThirdPartyCrashReporterMock.initializationError = ErrorMock("Initialization error") From f13b64e0b9209df876846c38bac73e408b700519 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Jan 2024 11:31:40 +0000 Subject: [PATCH 35/60] Add tvOS check --- DatadogInternal/Sources/DD.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index 5a9c77fcc0..2a1f6cd153 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -29,7 +29,7 @@ import OSLog /// Function printing `String` content to console. public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in - if #available(iOS 14.0, *) { + if #available(iOS 14.0, tvOS 14.0, *) { switch level { case .debug: Logger.datadog.debug("\(message, privacy: .private)") case .warn: Logger.datadog.warning("\(message, privacy: .private)") @@ -42,7 +42,7 @@ public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in } #if canImport(OSLog) -@available(iOS 14.0, *) +@available(iOS 14.0, tvOS 14.0, *) extension Logger { static let datadog = Logger(subsystem: "dd-sdk-ios", category: "insights") } From 81de468fcf119a47200df08bf0f3be21dd807f0f Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Jan 2024 13:01:14 +0000 Subject: [PATCH 36/60] Fix SR unit tests --- DatadogSessionReplay/Tests/SessionReplayTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Tests/SessionReplayTests.swift b/DatadogSessionReplay/Tests/SessionReplayTests.swift index c58c976226..4b5aa0a163 100644 --- a/DatadogSessionReplay/Tests/SessionReplayTests.swift +++ b/DatadogSessionReplay/Tests/SessionReplayTests.swift @@ -37,7 +37,7 @@ class SessionReplayTests: XCTestCase { func testWhenEnabledInNOPCore_itPrintsError() { let printFunction = PrintFunctionMock() consolePrint = printFunction.print - defer { consolePrint = { print($0) } } + defer { consolePrint = { message, _ in print(message) } } // When SessionReplay.enable(with: config, in: NOPDatadogCore()) From e08c2684a1384bc0fdc4ec99db1d26ed5b01f52c Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 17 Jan 2024 10:41:26 +0000 Subject: [PATCH 37/60] PR fixes --- DatadogInternal/Sources/DD.swift | 8 +++++++- DatadogLogs/Sources/ConsoleLogger.swift | 2 +- DatadogLogs/Sources/LoggerProtocol.swift | 14 ++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index 2a1f6cd153..fd10e21633 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -25,10 +25,13 @@ public struct DD { ) } +#if canImport(OSLog) import OSLog +#endif /// Function printing `String` content to console. public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in + #if canImport(OSLog) if #available(iOS 14.0, tvOS 14.0, *) { switch level { case .debug: Logger.datadog.debug("\(message, privacy: .private)") @@ -39,11 +42,14 @@ public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in } else { print(message) } + #else + print(message) + #endif } #if canImport(OSLog) @available(iOS 14.0, tvOS 14.0, *) extension Logger { - static let datadog = Logger(subsystem: "dd-sdk-ios", category: "insights") + static let datadog = Logger(subsystem: "dd-sdk-ios", category: "DatadogInternal") } #endif diff --git a/DatadogLogs/Sources/ConsoleLogger.swift b/DatadogLogs/Sources/ConsoleLogger.swift index 73f26b0929..d2a793cea0 100644 --- a/DatadogLogs/Sources/ConsoleLogger.swift +++ b/DatadogLogs/Sources/ConsoleLogger.swift @@ -64,7 +64,7 @@ internal final class ConsoleLogger: LoggerProtocol { if let errorString = errorString { log += "\n\nError details:\n\(errorString)" } - printFunction(log, level.asCoreLoggerLevel()) + printFunction(log, CoreLoggerLevel(logLevel: level)) } private func buildErrorString(error: DDError) -> String { diff --git a/DatadogLogs/Sources/LoggerProtocol.swift b/DatadogLogs/Sources/LoggerProtocol.swift index 423f611851..f0c7c55cb2 100644 --- a/DatadogLogs/Sources/LoggerProtocol.swift +++ b/DatadogLogs/Sources/LoggerProtocol.swift @@ -16,13 +16,15 @@ public enum LogLevel: Int, Codable { case warn case error case critical +} - func asCoreLoggerLevel() -> CoreLoggerLevel { - switch self { - case .debug, .info, .notice: return .debug - case .warn: return .warn - case .error: return .error - case .critical: return .critical +extension CoreLoggerLevel { + public init(logLevel: LogLevel) { + switch logLevel { + case .debug, .info, .notice: self = .debug + case .warn: self = .warn + case .error: self = .error + case .critical: self = .critical } } } From bcd003d8d3776fded314249535f0f4169fae03f1 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 17 Jan 2024 10:59:37 +0000 Subject: [PATCH 38/60] RUM-402 Remove benchmark tests --- BenchmarkTests/BenchmarkMocks.swift | 27 --- BenchmarkTests/BenchmarkTests.swift | 85 ------- .../LoggingBenchmarkTests.swift | 42 ---- .../DataCollection/RUMBenchmarkTests.swift | 36 --- .../TracingBenchmarkTests.swift | 41 ---- .../LoggingStorageBenchmarkTests.swift | 128 ---------- .../RUMStorageBenchmarkTests.swift | 90 ------- .../TracingStorageBenchmarkTests.swift | 127 ---------- .../DataUploaderBenchmarkTests.swift | 47 ---- Datadog/Datadog.xcodeproj/project.pbxproj | 225 ------------------ 10 files changed, 848 deletions(-) delete mode 100644 BenchmarkTests/BenchmarkMocks.swift delete mode 100644 BenchmarkTests/BenchmarkTests.swift delete mode 100644 BenchmarkTests/DataCollection/LoggingBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataCollection/RUMBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataCollection/TracingBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift delete mode 100644 BenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift diff --git a/BenchmarkTests/BenchmarkMocks.swift b/BenchmarkTests/BenchmarkMocks.swift deleted file mode 100644 index 629a7756c7..0000000000 --- a/BenchmarkTests/BenchmarkMocks.swift +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import DatadogInternal -@testable import DatadogCore - -extension PerformancePreset { - static let benchmarksPreset = PerformancePreset(batchSize: .small, uploadFrequency: .frequent, bundleType: .iOSApp) -} - -struct FeatureRequestBuilderMock: FeatureRequestBuilder { - let dataFormat = DataFormat(prefix: "", suffix: "", separator: "\n") - - func request(for events: [Event], with context: DatadogContext) -> URLRequest { - let builder = URLRequestBuilder( - url: .mockAny(), - queryItems: [.ddtags(tags: ["foo:bar"])], - headers: [] - ) - - let data = dataFormat.format(events.map { $0.data }) - return builder.uploadRequest(with: data) - } -} diff --git a/BenchmarkTests/BenchmarkTests.swift b/BenchmarkTests/BenchmarkTests.swift deleted file mode 100644 index e341527690..0000000000 --- a/BenchmarkTests/BenchmarkTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import HTTPServerMock -import DatadogCore -import DatadogLogs -import DatadogTrace -import DatadogRUM - -struct ServerConnectionError: Error { - let description: String -} - -/// Base class providing mock server instrumentation and SDK initialization. -class BenchmarkTests: XCTestCase { - /// Python server instance. - var server: ServerMock { BenchmarkTests.connectedServer! } - - override class func setUp() { - super.setUp() - do { - try connectToServerIfNotConnected() - } catch let error { - fatalError("Failed to connect to Python server: \(error)") - } - initializeSDKIfNotInitialized() - } - - // MARK: - SDK Initialization - - private static var isSDKInitialized = false - - private static func initializeSDKIfNotInitialized() { - if BenchmarkTests.isSDKInitialized { - return - } - - BenchmarkTests.isSDKInitialized = true - - let anyURL = connectedServer!.obtainUniqueRecordingSession().recordingURL - - Datadog.initialize( - with: Datadog.Configuration(clientToken: "rum-abc", env: "benchmarks"), - trackingConsent: .granted - ) - - RUM.enable(with: .init(applicationID: "rum-123", customEndpoint: anyURL)) - Logs.enable(with: .init(customEndpoint: anyURL)) - Trace.enable(with: .init(customEndpoint: anyURL)) - } - - // MARK: - `HTTPServerMock` connection - - private static var connectedServer: ServerMock? - - private static func connectToServerIfNotConnected() throws { - if BenchmarkTests.connectedServer != nil { - return - } - - let testsBundle = Bundle(for: BenchmarkTests.self) - guard let serverAddress = testsBundle.object(forInfoDictionaryKey: "MockServerAddress") as? String else { - throw ServerConnectionError(description: "Cannot obtain `MockServerAddress` from `Info.plist`") - } - - guard let serverURL = URL(string: "http://\(serverAddress)") else { - throw ServerConnectionError(description: "`MockServerAddress` obtained from `Info.plist` is invalid.") - } - - let serverProcessRunner = ServerProcessRunner(serverURL: serverURL) - guard let serverProcess = serverProcessRunner.waitUntilServerIsReachable() else { - throw ServerConnectionError( - description: "The server seems to be not running properly on \(serverURL.absoluteString)" - ) - } - - print("🌍 Connected to mock server on \(serverURL.absoluteString)") - - BenchmarkTests.connectedServer = ServerMock(serverProcess: serverProcess) - } -} diff --git a/BenchmarkTests/DataCollection/LoggingBenchmarkTests.swift b/BenchmarkTests/DataCollection/LoggingBenchmarkTests.swift deleted file mode 100644 index 9a9cb7b5dd..0000000000 --- a/BenchmarkTests/DataCollection/LoggingBenchmarkTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import DatadogLogs - -class LoggingBenchmarkTests: BenchmarkTests { - private let message = "foobar-message" - - func testCreatingOneLog() { - let logger = Logger.create() - - measure { - logger.info(message) - } - } - - func testCreatingOneLogWithAttributes() { - let logger = Logger.create() - (0..<16).forEach { index in - logger.addAttribute(forKey: "a\(index)", value: "v\(index)") - } - - measure { - logger.info(message) - } - } - - func testCreatingOneLogWithTags() { - let logger = Logger.create() - (0..<8).forEach { index in - logger.addTag(withKey: "t\(index)", value: "v\(index)") - } - - measure { - logger.info(message) - } - } -} diff --git a/BenchmarkTests/DataCollection/RUMBenchmarkTests.swift b/BenchmarkTests/DataCollection/RUMBenchmarkTests.swift deleted file mode 100644 index 10e2e35f0e..0000000000 --- a/BenchmarkTests/DataCollection/RUMBenchmarkTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import DatadogInternal -import DatadogRUM - -class RUMBenchmarkTests: BenchmarkTests { - var rum: RUMMonitorProtocol { RUMMonitor.shared() } - - func testCreatingOneRUMEvent() { - let viewController = UIViewController() - rum.startView(viewController: viewController) - - measure { - rum.addAction(type: .tap, name: "tap") - } - } - - func testCreatingOneRUMEventWithAttributes() { - let viewController = UIViewController() - rum.startView(viewController: viewController) - - var attributes: [AttributeKey: AttributeValue] = [:] - (0..<16).forEach { index in - attributes["a\(index)"] = "v\(index)" - } - - measure { - rum.addAction(type: .tap, name: "tap", attributes: attributes) - } - } -} diff --git a/BenchmarkTests/DataCollection/TracingBenchmarkTests.swift b/BenchmarkTests/DataCollection/TracingBenchmarkTests.swift deleted file mode 100644 index ebf16ea47e..0000000000 --- a/BenchmarkTests/DataCollection/TracingBenchmarkTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import DatadogCore -import XCTest - -import DatadogTrace - -class TracingBenchmarkTests: BenchmarkTests { - private let operationName = "foobar-span" - - func testCreatingAndEndingOneSpan() { - measure { - let testSpan = Tracer.shared().startSpan(operationName: operationName) - testSpan.finish() - } - } - - func testCreatingOneSpanWithBaggageItems() { - measure { - let testSpan = Tracer.shared().startSpan(operationName: operationName) - (0..<16).forEach { index in - testSpan.setBaggageItem(key: "a\(index)", value: "v\(index)") - } - testSpan.finish() - } - } - - func testCreatingOneSpanWithTags() { - measure { - let testSpan = Tracer.shared().startSpan(operationName: operationName) - (0..<8).forEach { index in - testSpan.setTag(key: "t\(index)", value: "v\(index)") - } - testSpan.finish() - } - } -} diff --git a/BenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift b/BenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift deleted file mode 100644 index d8df24edf0..0000000000 --- a/BenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import DatadogInternal - -@testable import DatadogLogs -@testable import DatadogCore - -class LoggingStorageBenchmarkTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional - private var queue: DispatchQueue! - private var directory: Directory! - private var writer: Writer! - private var reader: Reader! - // swiftlint:enable implicitly_unwrapped_optional - - override func setUpWithError() throws { - try super.setUpWithError() - self.directory = try Directory(withSubdirectoryPath: "logging-benchmark") - self.queue = DispatchQueue(label: "logging-benchmark") - - let storage = FeatureStorage( - featureName: "logging", - queue: queue, - directories: .init( - unauthorized: directory, - authorized: directory - ), - dateProvider: SystemDateProvider(), - performance: .benchmarksPreset, - encryption: nil, - telemetry: NOPTelemetry() - ) - - self.writer = storage.writer(for: .granted, forceNewBatch: false) - self.reader = storage.reader - - XCTAssertTrue(try directory.files().isEmpty) - } - - override func tearDown() { - try? FileManager.default.removeItem(at: directory.url) - queue = nil - directory = nil - writer = nil - reader = nil - super.tearDown() - } - - func testWritingLogsOnDisc() throws { - let log = createRandomizedLog() - - measure { - writer.write(value: log) - queue.sync {} // wait to complete async write - } - } - - func testReadingLogsFromDisc() throws { - while try directory.files().count < 10 { // `measureMetrics {}` is fired 10 times so 10 batch files are required - writer.write(value: createRandomizedLog()) - queue.sync {} // wait to complete async write - } - - // Wait enough time for `reader` to accept the youngest batch file - Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1) - - measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { - self.startMeasuring() - let batch = reader.readNextBatches(1).first - self.stopMeasuring() - - XCTAssertNotNil(batch, "Not enough batch files were created for this benchmark.") - - if let batch = batch { - reader.markBatchAsRead(batch, reason: .flushed) - } - } - } - - // MARK: - Helpers - - private func createRandomizedLog() -> LogEvent { - return LogEvent( - date: Date(), - status: .info, - message: "message \(Int.random(in: 0..<100))", - error: .init( - kind: nil, - message: "description", - stack: nil - ), - serviceName: "service-name", - environment: "benchmarks", - loggerName: "logger-name", - loggerVersion: "0.0.0", - threadName: "main", - applicationVersion: "0.0.0", - applicationBuildNumber: "0", - buildId: "0", - dd: .init(device: .init(architecture: "testArch")), - os: .init( - name: "OS", - version: "1.0.0", - build: "FFFFF" - ), - userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com", extraInfo: ["str": "value", "int": 11_235, "bool": true]), - networkConnectionInfo: .init( - reachability: .yes, - availableInterfaces: [.cellular], - supportsIPv4: true, - supportsIPv6: true, - isExpensive: false, - isConstrained: false - ), - mobileCarrierInfo: nil, - attributes: LogEvent.Attributes( - userAttributes: ["user.attribute": "value"], - internalAttributes: ["internal.attribute": "value"] - ), - tags: ["tag:value"] - ) - } -} diff --git a/BenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift b/BenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift deleted file mode 100644 index acc09206ce..0000000000 --- a/BenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import DatadogInternal -import DatadogRUM - -@testable import DatadogCore - -class RUMStorageBenchmarkTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional - private var queue: DispatchQueue! - private var directory: Directory! - private var writer: Writer! - private var reader: Reader! - // swiftlint:enable implicitly_unwrapped_optional - - override func setUpWithError() throws { - try super.setUpWithError() - self.directory = try Directory(withSubdirectoryPath: "rum-benchmark") - self.queue = DispatchQueue(label: "rum-benchmark") - - let storage = FeatureStorage( - featureName: "rum", - queue: queue, - directories: .init( - unauthorized: directory, - authorized: directory - ), - dateProvider: SystemDateProvider(), - performance: .benchmarksPreset, - encryption: nil, - telemetry: NOPTelemetry() - ) - - self.writer = storage.writer(for: .granted, forceNewBatch: false) - self.reader = storage.reader - - XCTAssertTrue(try directory.files().isEmpty) - } - - override func tearDown() { - try? FileManager.default.removeItem(at: directory.url) - queue = nil - directory = nil - writer = nil - reader = nil - super.tearDown() - } - - func testWritingRUMEventsOnDisc() throws { - let event: RUMViewEvent = .mockRandom() - - measure { - writer.write(value: event) - queue.sync {} // wait to complete async write - } - } - - func testReadingRUMEventsFromDisc() throws { - while try directory.files().count < 10 { // `measureMetrics {}` is fired 10 times so 10 batch files are required - writer.write(value: RUMViewEvent.mockRandom()) - queue.sync {} // wait to complete async write - } - - // Wait enough time for `reader` to accept the youngest batch file - Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1) - - measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { - self.startMeasuring() - let batch = reader.readNextBatches(1).first - self.stopMeasuring() - - XCTAssertNotNil(batch, "Not enough batch files were created for this benchmark.") - - if let batch = batch { - reader.markBatchAsRead(batch, reason: .flushed) - } - } - } -} - -extension Reader { - func readNextBatches(_ limit: Int = .max) -> [Batch] { - return readFiles(limit: limit).compactMap { readBatch(from: $0) } - } -} diff --git a/BenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift b/BenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift deleted file mode 100644 index 5a4c33b858..0000000000 --- a/BenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import DatadogInternal - -@testable import DatadogTrace -@testable import DatadogCore - -class TracingStorageBenchmarkTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional - private var queue: DispatchQueue! - private var directory: Directory! - private var writer: Writer! - private var reader: Reader! - // swiftlint:enable implicitly_unwrapped_optional - - override func setUpWithError() throws { - try super.setUpWithError() - self.directory = try Directory(withSubdirectoryPath: "tracing-benchmark") - self.queue = DispatchQueue(label: "tracing-benchmark") - - let storage = FeatureStorage( - featureName: "tracing", - queue: queue, - directories: .init( - unauthorized: directory, - authorized: directory - ), - dateProvider: SystemDateProvider(), - performance: .benchmarksPreset, - encryption: nil, - telemetry: NOPTelemetry() - ) - - self.writer = storage.writer(for: .granted, forceNewBatch: false) - self.reader = storage.reader - - XCTAssertTrue(try directory.files().isEmpty) - } - - override func tearDown() { - try? FileManager.default.removeItem(at: directory.url) - queue = nil - directory = nil - writer = nil - reader = nil - super.tearDown() - } - - func testWritingSpansOnDisc() throws { - let log = createRandomizedSpan() - - measure { - writer.write(value: log) - queue.sync {} // wait to complete async write - } - } - - func testReadingSpansFromDisc() throws { - while try directory.files().count < 10 { // `measureMetrics {}` is fired 10 times so 10 batch files are required - writer.write(value: createRandomizedSpan()) - queue.sync {} // wait to complete async write - } - - // Wait enough time for `reader` to accept the youngest batch file - Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1) - - measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { - self.startMeasuring() - let batch = reader.readNextBatches(1).first - self.stopMeasuring() - - XCTAssertNotNil(batch, "Not enough batch files were created for this benchmark.") - - if let batch = batch { - reader.markBatchAsRead(batch, reason: .flushed) - } - } - } - - // MARK: - Helpers - - private func createRandomizedSpan() -> SpanEvent { - let tracingUUIDGenerator = DefaultTraceIDGenerator() - return SpanEvent( - traceID: tracingUUIDGenerator.generate(), - spanID: tracingUUIDGenerator.generate(), - parentID: nil, - operationName: "span \(Int.random(in: 0..<100))", - serviceName: "service-name", - resource: "benchmarks", - startTime: Date(), - duration: Double.random(in: 0.0..<1.0), - isError: false, - source: "ios", - origin: nil, - samplingRate: 100, - isKept: true, - tracerVersion: "0.0.0", - applicationVersion: "0.0.0", - networkConnectionInfo: NetworkConnectionInfo( - reachability: .yes, - availableInterfaces: [.cellular], - supportsIPv4: true, - supportsIPv6: true, - isExpensive: false, - isConstrained: false - ), - mobileCarrierInfo: nil, - userInfo: .init( - id: "abc-123", - name: "foo", - email: "foo@bar.com", - extraInfo: [ - "info": .mockRandom(), - ] - ), - tags: [ - "tag": .mockRandom(), - ] - ) - } -} diff --git a/BenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift b/BenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift deleted file mode 100644 index 0671d36e4b..0000000000 --- a/BenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -import TestUtilities -import HTTPServerMock -import DatadogInternal -@testable import DatadogCore - -@available(iOS 13.0, *) -class DataUploaderBenchmarkTests: BenchmarkTests { - override func setUpWithError() throws { - try super.setUpWithError() - CreateTemporaryDirectory() - } - - override func tearDownWithError() throws { - DeleteTemporaryDirectory() - try super.tearDownWithError() - } - - /// NOTE: In RUMM-610 we noticed that due to internal `NSCache` used by the `URLSession` - /// requests memory was leaked after upload. This benchmark ensures that uploading data with - /// `DataUploader` leaves no memory footprint (the memory peak after upload is less or equal `0kB`). - func testUploadingDataToServer_leavesNoMemoryFootprint() throws { - let dataUploader = DataUploader( - httpClient: URLSessionClient(), - requestBuilder: FeatureRequestBuilderMock() - ) - - let context: DatadogContext = .mockAny() - - // `measure` runs 5 iterations - measure(metrics: [XCTMemoryMetric()]) { - // in each, 10 requests are done: - try? (0..<10).forEach { _ in - let events = [Event(data: Data(repeating: 0x41, count: 10 * 1_024 * 1_024))] - _ = try dataUploader.upload(events: events, context: context) - } - // After all, the baseline asserts `0kB` or less grow in Physical Memory. - // This makes sure that no request data is leaked (e.g. due to internal caching). - } - } -} diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index cabf1e58c3..26be547b51 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -252,9 +252,6 @@ 6136CB4B2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */; }; 6139CD712589FAFD007E8BB7 /* Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD702589FAFD007E8BB7 /* Retrying.swift */; }; 6139CD772589FEE3007E8BB7 /* RetryingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */; }; - 613BE0432563FB9E0015216C /* RUMBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613BE0422563FB9E0015216C /* RUMBenchmarkTests.swift */; }; - 613BE04A25640FF80015216C /* BenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613BE04925640FF80015216C /* BenchmarkTests.swift */; }; - 613BE06225642F790015216C /* RUMStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613BE06125642F790015216C /* RUMStorageBenchmarkTests.swift */; }; 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E792E2577B0F900DFCC17 /* Reader.swift */; }; 613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E793A2577B6EE00DFCC17 /* DataReader.swift */; }; 614396722A67D74F00197326 /* CoreMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* CoreMetrics.swift */; }; @@ -262,9 +259,6 @@ 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; 61441C0C24616DE9003D8BB8 /* Main iOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */; }; 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; - 61441C6D24619FE4003D8BB8 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; - 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */; }; - 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */; }; 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */; }; 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */; }; 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */; }; @@ -287,10 +281,7 @@ 614CADD72510BAC000B93D2D /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614CADD62510BAC000B93D2D /* Environment.swift */; }; 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614E9EB2244719FA007EE3E1 /* BundleType.swift */; }; 614ED36C260352DC00C8C519 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; - 6152C83E24BE1C91006A1679 /* HTTPServerMock in Frameworks */ = {isa = PBXBuildFile; productRef = 6152C83D24BE1C91006A1679 /* HTTPServerMock */; }; - 6152C84024BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */; }; 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; - 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Trace+objc.swift */; }; 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8A24A3568900233986 /* OTSpan+objc.swift */; }; @@ -360,7 +351,6 @@ 61A2CC2B2A4449300000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; 61A2CC302A4449CB0000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; 61A2CC312A4449D70000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; - 61A2CC322A445D8A0000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; 61A2CC332A44A5F60000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; 61A2CC342A44A6030000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; 61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */; }; @@ -434,7 +424,6 @@ 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */; }; 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */; }; 61D3E0EA277E0C58008BE766 /* KronosE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */; }; - 61D6FF7E24E53D3B00D0E375 /* BenchmarkMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */; }; 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */; }; 61DA8CA928609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; 61DA8CAA28609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; @@ -875,7 +864,6 @@ D2579592298ABCED008A1BE5 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579591298ABCED008A1BE5 /* XCTest.framework */; }; D2579595298AC912008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; D2579596298AC927008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; - D2579597298AD1A8008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; D2579599298AD95F008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; D257959A298AD967008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; D25CFA9829C4F41900E3A43D /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; @@ -908,7 +896,6 @@ D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; - D2790C7229DEFCF400D88DA9 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */; }; D27D81C12A5D415200281CC2 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; D27D81C22A5D415200281CC2 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; D27D81C32A5D415200281CC2 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; @@ -1451,8 +1438,6 @@ D2FB1258292E0F10005B13F8 /* TrackingConsentPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB1256292E0F0B005B13F8 /* TrackingConsentPublisherTests.swift */; }; D2FB125D292FBB56005B13F8 /* Datadog+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */; }; D2FB125E292FBB56005B13F8 /* Datadog+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */; }; - E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */; }; - E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */; }; E143CCAF27D236F600F4018A /* CITestIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */; }; E1C853142AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */; }; E1C853152AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */; }; @@ -1551,13 +1536,6 @@ remoteGlobalIDString = 61441C0124616DE9003D8BB8; remoteInfo = Example; }; - 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 61133B79242393DE00786299 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 61441C0124616DE9003D8BB8; - remoteInfo = Example; - }; 6158155A2AB4534F002C60D7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; @@ -2109,9 +2087,6 @@ 61378BB22555337900F28837 /* DatadogSDKTesting.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogSDKTesting.local.xcconfig; sourceTree = ""; }; 6139CD702589FAFD007E8BB7 /* Retrying.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retrying.swift; sourceTree = ""; }; 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryingTests.swift; sourceTree = ""; }; - 613BE0422563FB9E0015216C /* RUMBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMBenchmarkTests.swift; sourceTree = ""; }; - 613BE04925640FF80015216C /* BenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkTests.swift; sourceTree = ""; }; - 613BE06125642F790015216C /* RUMStorageBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMStorageBenchmarkTests.swift; sourceTree = ""; }; 613C6B8F2768FDDE00870CBF /* Sampler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sampler.swift; sourceTree = ""; }; 613C6B912768FF3100870CBF /* SamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplerTests.swift; sourceTree = ""; }; 613E792E2577B0F900DFCC17 /* Reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reader.swift; sourceTree = ""; }; @@ -2129,10 +2104,7 @@ 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Main iOS.storyboard"; sourceTree = ""; }; 61441C0D24616DEC003D8BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61441C1224616DEC003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogBenchmarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61441C6C24619FE4003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingBenchmarkTests.swift; sourceTree = ""; }; - 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingStorageBenchmarkTests.swift; sourceTree = ""; }; 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleOutputInterceptor.swift; sourceTree = ""; }; 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Disabling.swift"; sourceTree = ""; }; 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugTracingViewController.swift; sourceTree = ""; }; @@ -2152,7 +2124,6 @@ 614CADD62510BAC000B93D2D /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = ../Carthage/Build/CrashReporter.xcframework; sourceTree = ""; }; - 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploaderBenchmarkTests.swift; sourceTree = ""; }; 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogBenchmarkTests.xcconfig; sourceTree = ""; }; 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; @@ -2329,7 +2300,6 @@ 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPPacketTests.swift; sourceTree = ""; }; 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorageTests.swift; sourceTree = ""; }; 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KronosE2ETests.swift; sourceTree = ""; }; - 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkMocks.swift; sourceTree = ""; }; 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatusTests.swift; sourceTree = ""; }; 61DA8CA828609C5B0074A606 /* Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Directories.swift; sourceTree = ""; }; 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoriesTests.swift; sourceTree = ""; }; @@ -2694,8 +2664,6 @@ D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datadog+Internal.swift"; sourceTree = ""; }; D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = ""; }; E11625D727B681D200E428C6 /* CITestIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CITestIntegration.swift; sourceTree = ""; }; - E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingBenchmarkTests.swift; sourceTree = ""; }; - E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingStorageBenchmarkTests.swift; sourceTree = ""; }; E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CITestIntegrationTests.swift; sourceTree = ""; }; E179FB4D28F80A6400CC2698 /* PerformanceMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetric.swift; sourceTree = ""; }; E1B082CB25641DF9002DB9D2 /* Example.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Example.xcconfig; sourceTree = ""; }; @@ -2786,18 +2754,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 61441C6524619FE4003D8BB8 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 61A2CC322A445D8A0000FF25 /* DatadogRUM.framework in Frameworks */, - D2579597298AD1A8008A1BE5 /* TestUtilities.framework in Frameworks */, - 6152C83E24BE1C91006A1679 /* HTTPServerMock in Frameworks */, - 61441C6D24619FE4003D8BB8 /* DatadogCore.framework in Frameworks */, - 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 61569793256CF6C300C6AADA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -3666,7 +3622,6 @@ 6170DC1425C18663003AED5C /* DatadogCrashReportingTests */, 3CE11A3B29F7BEE700202522 /* DatadogWebViewTracking */, 3CE11A3C29F7BEF300202522 /* DatadogWebViewTrackingTests */, - 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */, 61133C07242397F200786299 /* TargetSupport */, 61441C0324616DE9003D8BB8 /* Example */, 6199362C265BA959009D7EA8 /* E2E */, @@ -3685,7 +3640,6 @@ 61133B8B242393DE00786299 /* DatadogCoreTests iOS.xctest */, 61133BF0242397DA00786299 /* DatadogObjc.framework */, 61441C0224616DE9003D8BB8 /* Example iOS.app */, - 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */, 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */, 61B7885C25C180CB002675B5 /* DatadogCrashReportingTests iOS.xctest */, 6199362B265BA958009D7EA8 /* E2E.app */, @@ -4120,34 +4074,6 @@ path = Utils; sourceTree = ""; }; - 613BE07A25643C040015216C /* DataCollection */ = { - isa = PBXGroup; - children = ( - 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */, - E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */, - 613BE0422563FB9E0015216C /* RUMBenchmarkTests.swift */, - ); - path = DataCollection; - sourceTree = ""; - }; - 613BE07B25643C080015216C /* DataUpload */ = { - isa = PBXGroup; - children = ( - 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */, - ); - path = DataUpload; - sourceTree = ""; - }; - 613BE07C25643C100015216C /* DataStorage */ = { - isa = PBXGroup; - children = ( - 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */, - E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */, - 613BE06125642F790015216C /* RUMStorageBenchmarkTests.swift */, - ); - path = DataStorage; - sourceTree = ""; - }; 613E79412577C08900DFCC17 /* Writing */ = { isa = PBXGroup; children = ( @@ -4248,19 +4174,6 @@ path = DatadogBenchmarkTests; sourceTree = ""; }; - 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */ = { - isa = PBXGroup; - children = ( - 613BE04925640FF80015216C /* BenchmarkTests.swift */, - 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */, - 613BE07A25643C040015216C /* DataCollection */, - 613BE07B25643C080015216C /* DataUpload */, - 613BE07C25643C100015216C /* DataStorage */, - ); - name = DatadogBenchmarkTests; - path = ../BenchmarkTests; - sourceTree = ""; - }; 61441C8F2461A648003D8BB8 /* Utils */ = { isa = PBXGroup; children = ( @@ -6156,27 +6069,6 @@ productReference = 61441C0224616DE9003D8BB8 /* Example iOS.app */; productType = "com.apple.product-type.application"; }; - 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */; - buildPhases = ( - 61441C6424619FE4003D8BB8 /* Sources */, - 61441C6524619FE4003D8BB8 /* Frameworks */, - 61441C6624619FE4003D8BB8 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 61441C7524619FED003D8BB8 /* PBXTargetDependency */, - ); - name = DatadogBenchmarkTests; - packageProductDependencies = ( - 6152C83D24BE1C91006A1679 /* HTTPServerMock */, - ); - productName = DatadogBenchmarkTests; - productReference = 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 618F983F265BC486009959F8 /* E2EInstrumentationTests */ = { isa = PBXNativeTarget; buildConfigurationList = 618F9847265BC486009959F8 /* Build configuration list for PBXNativeTarget "E2EInstrumentationTests" */; @@ -6767,10 +6659,6 @@ CreatedOnToolsVersion = 11.4; LastSwiftMigration = 1200; }; - 61441C6724619FE4003D8BB8 = { - CreatedOnToolsVersion = 11.4; - TestTargetID = 61441C0124616DE9003D8BB8; - }; 618F983F265BC486009959F8 = { CreatedOnToolsVersion = 12.5; TestTargetID = 6199362A265BA958009D7EA8; @@ -6862,7 +6750,6 @@ D2CB6E0A27C50EAE00A62B57 /* DatadogCore tvOS */, D2CB6F9227C5217A00A62B57 /* DatadogObjc tvOS */, D2CB6ED327C520D400A62B57 /* DatadogCoreTests tvOS */, - 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */, 61441C0124616DE9003D8BB8 /* Example iOS */, D24067F827CE6C9E00C04F44 /* Example tvOS */, 6199362A265BA958009D7EA8 /* E2E */, @@ -6938,13 +6825,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 61441C6624619FE4003D8BB8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 618F983E265BC486009959F8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -7800,23 +7680,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 61441C6424619FE4003D8BB8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */, - 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */, - D2790C7229DEFCF400D88DA9 /* RUMDataModelMocks.swift in Sources */, - 6152C84024BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift in Sources */, - E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */, - 613BE0432563FB9E0015216C /* RUMBenchmarkTests.swift in Sources */, - E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */, - 613BE04A25640FF80015216C /* BenchmarkTests.swift in Sources */, - 613BE06225642F790015216C /* RUMStorageBenchmarkTests.swift in Sources */, - 61D6FF7E24E53D3B00D0E375 /* BenchmarkMocks.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 618F983C265BC486009959F8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -9001,11 +8864,6 @@ target = 61441C0124616DE9003D8BB8 /* Example iOS */; targetProxy = 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */; }; - 61441C7524619FED003D8BB8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61441C0124616DE9003D8BB8 /* Example iOS */; - targetProxy = 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */; - }; 6158155B2AB4534F002C60D7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61441C0124616DE9003D8BB8 /* Example iOS */; @@ -9979,72 +9837,6 @@ }; name = Integration; }; - 61441C7124619FE4003D8BB8 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; - }; - name = Debug; - }; - 61441C7224619FE4003D8BB8 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; - }; - name = Release; - }; - 61441C7324619FE4003D8BB8 /* Integration */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; - }; - name = Integration; - }; 618F9848265BC486009959F8 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */; @@ -12762,16 +12554,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 61441C7124619FE4003D8BB8 /* Debug */, - 61441C7224619FE4003D8BB8 /* Release */, - 61441C7324619FE4003D8BB8 /* Integration */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 618F9847265BC486009959F8 /* Build configuration list for PBXNativeTarget "E2EInstrumentationTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13063,13 +12845,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - 6152C83D24BE1C91006A1679 /* HTTPServerMock */ = { - isa = XCSwiftPackageProductDependency; - productName = HTTPServerMock; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 61133B79242393DE00786299 /* Project object */; } From 84c4009894dad45566e6ca048a0f1760b2d74028 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 17 Jan 2024 16:13:36 +0000 Subject: [PATCH 39/60] RUM-402 Remove remaining files and configs for benchmark tests --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 -- .../xcschemes/DatadogBenchmarkTests.xcscheme | 198 ------------------ .../DatadogBenchmarkTests.xcconfig | 8 - .../DatadogBenchmarkTests/Info.plist | 24 --- bitrise.yml | 11 - 5 files changed, 253 deletions(-) delete mode 100644 Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme delete mode 100644 Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig delete mode 100644 Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 26be547b51..3b1c031575 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -2104,7 +2104,6 @@ 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Main iOS.storyboard"; sourceTree = ""; }; 61441C0D24616DEC003D8BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61441C1224616DEC003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61441C6C24619FE4003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleOutputInterceptor.swift; sourceTree = ""; }; 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Disabling.swift"; sourceTree = ""; }; 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugTracingViewController.swift; sourceTree = ""; }; @@ -2124,7 +2123,6 @@ 614CADD62510BAC000B93D2D /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = ../Carthage/Build/CrashReporter.xcframework; sourceTree = ""; }; - 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogBenchmarkTests.xcconfig; sourceTree = ""; }; 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519262461BCE7002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; @@ -3781,7 +3779,6 @@ 6170DC0525C184FA003AED5C /* DatadogCrashReporting */, 61133B8F242393DE00786299 /* DatadogTests */, 6170DC0625C184FA003AED5C /* DatadogCrashReportingTests */, - 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */, 61441C9E2461AF4D003D8BB8 /* Example */, 61993640265BAC34009D7EA8 /* E2E */, 61993670265BBF19009D7EA8 /* E2ETests */, @@ -4165,15 +4162,6 @@ path = Example; sourceTree = ""; }; - 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */ = { - isa = PBXGroup; - children = ( - 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */, - 61441C6C24619FE4003D8BB8 /* Info.plist */, - ); - path = DatadogBenchmarkTests; - sourceTree = ""; - }; 61441C8F2461A648003D8BB8 /* Utils */ = { isa = PBXGroup; children = ( diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme deleted file mode 100644 index 4238ff2118..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig b/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig deleted file mode 100644 index d6cdbf1209..0000000000 --- a/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig +++ /dev/null @@ -1,8 +0,0 @@ -// Add common settings from Datadog.xcconfig -#include "../xcconfigs/Datadog.xcconfig" - -// Add server mock instrumentation settings (generated from pre-action) -#include "../xcconfigs/MockServerAddress.local.xcconfig" - -// Add DatadogSDKTesting instrumentation (if available in current environment) -#include? "../xcconfigs/DatadogSDKTesting.local.xcconfig" diff --git a/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist b/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist deleted file mode 100644 index e004e59be4..0000000000 --- a/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - MockServerAddress - $(MOCK_SERVER_ADDRESS) - - diff --git a/bitrise.yml b/bitrise.yml index df1f058905..e6a5f01ac1 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -300,17 +300,6 @@ workflows: set -e make prepare-integration-tests ./tools/config/generate-http-server-mock-config.sh - - xcode-test: - title: Run benchmarks - DatadogBenchmarkTests on iOS Simulator - run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' - inputs: - - scheme: DatadogBenchmarkTests - - destination: platform=iOS Simulator,name=iPhone 11,OS=latest - - should_build_before_test: 'no' - - is_clean_build: 'no' - - generate_code_coverage_files: 'yes' - - project_path: Datadog.xcworkspace - - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Benchmark-tests.html" - xcode-test: title: Run integration tests for RUM, Logging, Tracing and SR (on iOS Simulator) run_if: '{{enveq "DD_RUN_INTEGRATION_TESTS" "1"}}' From e9339c83196f94630966a8ec2ac81569daf5623a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:57:11 +0000 Subject: [PATCH 40/60] Bump gitpython from 3.1.37 to 3.1.41 in /tools/distribution Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.37 to 3.1.41. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.37...3.1.41) --- updated-dependencies: - dependency-name: gitpython dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- tools/distribution/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/distribution/requirements.txt b/tools/distribution/requirements.txt index 6d65549498..884b308d41 100644 --- a/tools/distribution/requirements.txt +++ b/tools/distribution/requirements.txt @@ -1,5 +1,5 @@ gitdb==4.0.10 -GitPython==3.1.37 +GitPython==3.1.41 smmap==5.0.0 packaging==23.1 pytest==7.4.0 From e2733dd67cb8117796db619174a3acb9106fb022 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 19 Jan 2024 11:22:18 +0100 Subject: [PATCH 41/60] RUM-2745 Add 502, 503 and 507 to the list of retryable status codes --- .../Sources/Core/Upload/DataUploadStatus.swift | 15 ++++++++++++--- .../Core/Upload/DataUploadStatusTests.swift | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift index 42203a55e3..333ef7d36c 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift @@ -24,8 +24,14 @@ private enum HTTPResponseStatusCode: Int { case tooManyRequests = 429 /// The server encountered an unexpected condition. case internalServerError = 500 + /// The server received an invalid response from another server. + case badGateway = 502 /// The server is not ready to handle the request probably because it is overloaded. case serviceUnavailable = 503 + /// The server (a gateway or proxy) did not receive a timely response from an upstream server. + case gatewayTimeout = 504 + /// The server is unable to complete a request due to a lack of available storage space. + case insufficientStorage = 507 /// An unexpected status code. case unexpected = -999 @@ -38,6 +44,9 @@ private enum HTTPResponseStatusCode: Int { case .requestTimeout, .tooManyRequests, .internalServerError, .serviceUnavailable: // Retry - it's a temporary server or connection issue that might disappear on next attempt. return true + case .badGateway, .gatewayTimeout, .insufficientStorage: + // RUM-2745: SDK is expected to not lose data upon receiving these status codes + return true case .unexpected: // This shouldn't happen, but if receiving an unexpected status code we do not retry. // This is safer than retrying as we don't know if the issue is coming from the client or server. @@ -126,11 +135,11 @@ extension DataUploadError { return nil case .unauthorized, .forbidden: self = .unauthorized - case .internalServerError, .serviceUnavailable: - // These codes mean Datadog service issue - do not produce SDK error as this is already monitored by other means. + case .internalServerError, .serviceUnavailable, .badGateway, .gatewayTimeout, .insufficientStorage: + // These codes indicate Datadog service issue - so do not produce error as there is no fix reqiured for SDK return nil case .badRequest, .payloadTooLarge, .tooManyRequests, .requestTimeout: - // These codes mean that something wrong is happening either in the SDK or on the server - produce an error. + // These codes might indicate SDK issue - so produce an error so we send it through telemetry. self = .httpError(statusCode: code) case .unexpected: return nil diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift index d35bf58739..91156d8a3d 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift @@ -23,7 +23,10 @@ class DataUploadStatusTests: XCTestCase { 408, // REQUEST TIMEOUT 429, // TOO MANY REQUESTS 500, // INTERNAL SERVER ERROR + 502, // BAD GATEWAY 503, // SERVICE UNAVAILABLE + 504, // GATEWAY TIMEOUT + 507, // INSUFFICIENT STORAGE ] private lazy var expectedStatusCodes = statusCodesExpectingNoRetry + statusCodesExpectingRetry From 777dc78deea77239830191cfb2feec6098348922 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 19 Jan 2024 11:29:29 +0100 Subject: [PATCH 42/60] RUM-2745 Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c3dc15ec..9923f972e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [FIX] RUM session not being linked to spans. See [#1615][] - [FEATURE] Allow stopping a core instance. See [#1541][] +- [IMPROVEMENT] Add extra HTTP codes to the list of retryable status codes. See [#1639][] # 2.6.0 / 09-01-2024 - [FEATURE] Add `currentSessionID(completion:)` accessor to access the current session ID. @@ -574,6 +575,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1594]: https://github.com/DataDog/dd-sdk-ios/pull/1594 [#1536]: https://github.com/DataDog/dd-sdk-ios/pull/1536 [#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609 +[#1639]: https://github.com/DataDog/dd-sdk-ios/pull/1639 [#1615]: https://github.com/DataDog/dd-sdk-ios/pull/1615 [#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 [#1541]: https://github.com/DataDog/dd-sdk-ios/pull/1541 From 949d3cfbea9580856adc91a183e1871c0bb6457b Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Fri, 19 Jan 2024 11:58:58 -0500 Subject: [PATCH 43/60] chore: Update RUM model schema to latest version Needed for Unity RUM. refs: RUM-2662 --- .../Datadog/Mocks/RUMDataModelMocks.swift | 1 + .../Sources/RUM/RUMDataModels+objc.swift | 45 ++++++++++++++++++- .../Sources/DataModels/RUMDataModels.swift | 21 ++++++++- .../DataModels/RUMDataModelsMapping.swift | 1 + .../Integrations/TelemetryReceiver.swift | 1 + .../Tests/Mocks/RUMDataModelMocks.swift | 1 + 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 335f7310e8..4f77e9b098 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -543,6 +543,7 @@ extension TelemetryConfigurationEvent: RandomMockable { useExcludedActivityUrls: nil, useFirstPartyHosts: .mockRandom(), useLocalEncryption: .mockRandom(), + usePartitionedCrossSiteSessionCookie: .mockRandom(), useProxy: .mockRandom(), useSecureSessionCookie: nil, useTracing: .mockRandom(), diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 82e5ce2acb..05b84a218b 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -627,6 +627,7 @@ public enum DDRUMActionEventContainerSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .roku: self = .roku + case .unity: self = .unity } } @@ -638,6 +639,7 @@ public enum DDRUMActionEventContainerSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -647,6 +649,7 @@ public enum DDRUMActionEventContainerSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -849,6 +852,7 @@ public enum DDRUMActionEventSource: Int { case .flutter?: self = .flutter case .reactNative?: self = .reactNative case .roku?: self = .roku + case .unity?: self = .unity } } @@ -861,6 +865,7 @@ public enum DDRUMActionEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -871,6 +876,7 @@ public enum DDRUMActionEventSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -1364,6 +1370,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .roku: self = .roku + case .unity: self = .unity } } @@ -1375,6 +1382,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -1384,6 +1392,7 @@ public enum DDRUMErrorEventContainerSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -1960,6 +1969,7 @@ public enum DDRUMErrorEventSource: Int { case .flutter?: self = .flutter case .reactNative?: self = .reactNative case .roku?: self = .roku + case .unity?: self = .unity } } @@ -1972,6 +1982,7 @@ public enum DDRUMErrorEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -1982,6 +1993,7 @@ public enum DDRUMErrorEventSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -2475,6 +2487,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .roku: self = .roku + case .unity: self = .unity } } @@ -2486,6 +2499,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -2495,6 +2509,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -2718,6 +2733,7 @@ public enum DDRUMLongTaskEventSource: Int { case .flutter?: self = .flutter case .reactNative?: self = .reactNative case .roku?: self = .roku + case .unity?: self = .unity } } @@ -2730,6 +2746,7 @@ public enum DDRUMLongTaskEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -2740,6 +2757,7 @@ public enum DDRUMLongTaskEventSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -3241,6 +3259,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .roku: self = .roku + case .unity: self = .unity } } @@ -3252,6 +3271,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -3261,6 +3281,7 @@ public enum DDRUMResourceEventContainerSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -3847,6 +3868,7 @@ public enum DDRUMResourceEventSource: Int { case .flutter?: self = .flutter case .reactNative?: self = .reactNative case .roku?: self = .roku + case .unity?: self = .unity } } @@ -3859,6 +3881,7 @@ public enum DDRUMResourceEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -3869,6 +3892,7 @@ public enum DDRUMResourceEventSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -4402,6 +4426,7 @@ public enum DDRUMViewEventContainerSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .roku: self = .roku + case .unity: self = .unity } } @@ -4413,6 +4438,7 @@ public enum DDRUMViewEventContainerSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -4422,6 +4448,7 @@ public enum DDRUMViewEventContainerSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -4710,6 +4737,7 @@ public enum DDRUMViewEventSource: Int { case .flutter?: self = .flutter case .reactNative?: self = .reactNative case .roku?: self = .roku + case .unity?: self = .unity } } @@ -4722,6 +4750,7 @@ public enum DDRUMViewEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .roku: return .roku + case .unity: return .unity } } @@ -4732,6 +4761,7 @@ public enum DDRUMViewEventSource: Int { case flutter case reactNative case roku + case unity } @objc @@ -5304,6 +5334,7 @@ public enum DDTelemetryErrorEventSource: Int { case .browser: self = .browser case .flutter: self = .flutter case .reactNative: self = .reactNative + case .unity: self = .unity } } @@ -5314,6 +5345,7 @@ public enum DDTelemetryErrorEventSource: Int { case .browser: return .browser case .flutter: return .flutter case .reactNative: return .reactNative + case .unity: return .unity } } @@ -5322,6 +5354,7 @@ public enum DDTelemetryErrorEventSource: Int { case browser case flutter case reactNative + case unity } @objc @@ -5498,6 +5531,7 @@ public enum DDTelemetryDebugEventSource: Int { case .browser: self = .browser case .flutter: self = .flutter case .reactNative: self = .reactNative + case .unity: self = .unity } } @@ -5508,6 +5542,7 @@ public enum DDTelemetryDebugEventSource: Int { case .browser: return .browser case .flutter: return .flutter case .reactNative: return .reactNative + case .unity: return .unity } } @@ -5516,6 +5551,7 @@ public enum DDTelemetryDebugEventSource: Int { case browser case flutter case reactNative + case unity } @objc @@ -5675,6 +5711,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .browser: self = .browser case .flutter: self = .flutter case .reactNative: self = .reactNative + case .unity: self = .unity } } @@ -5685,6 +5722,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .browser: return .browser case .flutter: return .flutter case .reactNative: return .reactNative + case .unity: return .unity } } @@ -5693,6 +5731,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case browser case flutter case reactNative + case unity } @objc @@ -5939,6 +5978,10 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.useLocalEncryption as NSNumber? } + @objc public var usePartitionedCrossSiteSessionCookie: NSNumber? { + root.swiftModel.telemetry.configuration.usePartitionedCrossSiteSessionCookie as NSNumber? + } + @objc public var useProxy: NSNumber? { set { root.swiftModel.telemetry.configuration.useProxy = newValue?.boolValue } get { root.swiftModel.telemetry.configuration.useProxy as NSNumber? } @@ -6080,4 +6123,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/49a2345f61a948013208d66a0fa9bad15a8c8fab +// Generated from https://github.com/DataDog/rum-events-format/tree/83f8760b46e9a117b5975cfb592b1803d643ee3e diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index cd790c156d..a1b747f1ed 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -357,6 +357,7 @@ public struct RUMActionEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// Attributes of the view's container @@ -420,6 +421,7 @@ public struct RUMActionEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// View properties @@ -643,6 +645,7 @@ public struct RUMErrorEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// Attributes of the view's container @@ -885,6 +888,7 @@ public struct RUMErrorEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// View properties @@ -1132,6 +1136,7 @@ public struct RUMLongTaskEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// Attributes of the view's container @@ -1213,6 +1218,7 @@ public struct RUMLongTaskEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// View properties @@ -1444,6 +1450,7 @@ public struct RUMResourceEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// Attributes of the view's container @@ -1737,6 +1744,7 @@ public struct RUMResourceEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// View properties @@ -1999,6 +2007,7 @@ public struct RUMViewEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// Attributes of the view's container @@ -2118,6 +2127,7 @@ public struct RUMViewEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case roku = "roku" + case unity = "unity" } /// View properties @@ -2581,6 +2591,7 @@ public struct TelemetryErrorEvent: RUMDataModel { case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" } /// The telemetry log information @@ -2730,6 +2741,7 @@ public struct TelemetryDebugEvent: RUMDataModel { case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" } /// The telemetry log information @@ -2896,6 +2908,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case browser = "browser" case flutter = "flutter" case reactNative = "react-native" + case unity = "unity" } /// The telemetry configuration information @@ -3048,7 +3061,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether beforeSend callback function is used public let useBeforeSend: Bool? - /// Whether a secure cross-site session cookie is used + /// Whether a secure cross-site session cookie is used (deprecated) public let useCrossSiteSessionCookie: Bool? /// Whether the request origins list to ignore when computing the page activity is used @@ -3060,6 +3073,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Whether local encryption is used public let useLocalEncryption: Bool? + /// Whether a partitioned secure cross-site session cookie is used + public let usePartitionedCrossSiteSessionCookie: Bool? + /// Whether a proxy is used public var useProxy: Bool? @@ -3125,6 +3141,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case useExcludedActivityUrls = "use_excluded_activity_urls" case useFirstPartyHosts = "use_first_party_hosts" case useLocalEncryption = "use_local_encryption" + case usePartitionedCrossSiteSessionCookie = "use_partitioned_cross_site_session_cookie" case useProxy = "use_proxy" case useSecureSessionCookie = "use_secure_session_cookie" case useTracing = "use_tracing" @@ -3538,4 +3555,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/49a2345f61a948013208d66a0fa9bad15a8c8fab +// Generated from https://github.com/DataDog/rum-events-format/tree/83f8760b46e9a117b5975cfb592b1803d643ee3e diff --git a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift index f1a3d1f46e..e1f14665f1 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift @@ -54,6 +54,7 @@ internal extension RUMViewEvent.Source { case .reactNative: return .reactNative case .flutter: return .flutter case .roku: return .roku + case .unity: return .unity } } } diff --git a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift index 4ade30655f..c6c5cd7533 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift @@ -304,6 +304,7 @@ private extension TelemetryConfigurationEvent.Telemetry.Configuration { useExcludedActivityUrls: nil, useFirstPartyHosts: configuration.useFirstPartyHosts, useLocalEncryption: configuration.useLocalEncryption, + usePartitionedCrossSiteSessionCookie: nil, useProxy: configuration.useProxy, useSecureSessionCookie: nil, useTracing: configuration.useTracing, diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index 13411d04a4..ab2dea5e77 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -556,6 +556,7 @@ extension TelemetryConfigurationEvent: RandomMockable { useExcludedActivityUrls: nil, useFirstPartyHosts: .mockRandom(), useLocalEncryption: .mockRandom(), + usePartitionedCrossSiteSessionCookie: nil, useProxy: .mockRandom(), useSecureSessionCookie: nil, useTracing: .mockRandom(), From a33837fd7dbabe00ba86d7e59b473f6feecdeffc Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 22 Jan 2024 12:05:50 +0100 Subject: [PATCH 44/60] Add make option to generate RUM or SR models for certain commit of `rum-events-format` --- Makefile | 6 ++++-- tools/rum-models-generator/run.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 1dedd57754..f5e7907295 100644 --- a/Makefile +++ b/Makefile @@ -110,9 +110,10 @@ test-xcframeworks: @cd dependency-manager-tests/xcframeworks && $(MAKE) # Generate RUM data models from rum-events-format JSON Schemas +# - run with `git_ref=` argument to generate models for given schema commit or branch name (default is 'master'). rum-models-generate: @echo "⚙️ Generating RUM models..." - ./tools/rum-models-generator/run.py generate rum + ./tools/rum-models-generator/run.py generate rum --git_ref=$(if $(git_ref),$(git_ref),master) @echo "OK 👌" # Verify if RUM data models follow rum-events-format JSON Schemas @@ -122,9 +123,10 @@ rum-models-verify: @echo "OK 👌" # Generate Session Replay data models from rum-events-format JSON Schemas +# - run with `git_ref=` argument to generate models for given schema commit or branch name (default is 'master'). sr-models-generate: @echo "⚙️ Generating Session Replay models..." - ./tools/rum-models-generator/run.py generate sr + ./tools/rum-models-generator/run.py generate sr --git_ref=$(if $(git_ref),$(git_ref),master) @echo "OK 👌" # Verify if Session Replay data models follow rum-events-format JSON Schemas diff --git a/tools/rum-models-generator/run.py b/tools/rum-models-generator/run.py index ebadc2fa21..bb5b4dd7b6 100755 --- a/tools/rum-models-generator/run.py +++ b/tools/rum-models-generator/run.py @@ -37,6 +37,9 @@ class Context: # Resolved path to JSON schema describing Session Replay events sr_schema_path: str + # Git reference to clone schemas repo at. + git_ref: str + # Resolved path to source code file with RUM model definitions (Swift) rum_swift_generated_file_path: str @@ -50,6 +53,7 @@ def __repr__(self): return f""" - cli_executable_path = {self.cli_executable_path}, - rum_schema_path = {self.rum_schema_path} + - git_ref = {self.git_ref} - sr_schema_path = {self.sr_schema_path} - rum_swift_generated_file_path = {self.rum_swift_generated_file_path} - rum_objc_generated_file_path = {self.rum_objc_generated_file_path} @@ -159,7 +163,7 @@ def validate_code(ctx: Context, language: str, convention: str, json_schema: str def generate_rum_models(ctx: Context): - sha = clone_schemas_repo(git_ref='master') + sha = clone_schemas_repo(git_ref=ctx.git_ref) with open(ctx.rum_swift_generated_file_path, 'w') as file: code = generate_code(ctx, language='swift', convention='rum', json_schema=ctx.rum_schema_path, git_sha=sha) @@ -171,7 +175,7 @@ def generate_rum_models(ctx: Context): def generate_sr_models(ctx: Context): - sha = clone_schemas_repo(git_ref='master') + sha = clone_schemas_repo(git_ref=ctx.git_ref) with open(ctx.sr_swift_generated_file_path, 'w') as file: code = generate_code(ctx, language='swift', convention='sr', json_schema=ctx.sr_schema_path, git_sha=sha) @@ -213,6 +217,7 @@ def validate_sr_models(ctx: Context): parser = argparse.ArgumentParser() parser.add_argument("command", choices=['generate', 'verify'], help="Run mode") parser.add_argument("product", choices=['rum', 'sr'], help="Either 'rum' (RUM) or 'sr' (Session Replay)") + parser.add_argument("--git_ref", help="The git reference to clone `rum-events-format` repo at (only effective for `generate` command).") args = parser.parse_args() try: @@ -220,6 +225,7 @@ def validate_sr_models(ctx: Context): cli_executable_path=build_swift_cli(), rum_schema_path=os.path.abspath(f'{script_dir}/{RUM_SCHEMA_PATH}'), sr_schema_path=os.path.abspath(f'{script_dir}/{SR_SCHEMA_PATH}'), + git_ref=args.git_ref if args.command else None, rum_swift_generated_file_path=os.path.abspath(f'{repository_root}/{RUM_SWIFT_GENERATED_FILE_PATH}'), rum_objc_generated_file_path=os.path.abspath(f'{repository_root}/{RUM_OBJC_GENERATED_FILE_PATH}'), sr_swift_generated_file_path=os.path.abspath(f'{repository_root}/{SR_SWIFT_GENERATED_FILE_PATH}'), From 8b13817f361eb50db61a3ae26ea62458f38ec999 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 22 Jan 2024 12:32:06 +0100 Subject: [PATCH 45/60] [RUM models generation] Support enum cases that start with digits --- .../Swift/JSONToSwiftTypeTransformer.swift | 11 +- .../JSONToSwiftTypeTransformerTests.swift | 108 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift index 9122f3379a..84b6adb1b6 100644 --- a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift @@ -92,8 +92,17 @@ internal class JSONToSwiftTypeTransformer { cases: jsonEnumeration.values.map { value in switch value { case .string(let value): - return SwiftEnum.Case(label: value, rawValue: .string(value: value)) + // In Swift, enum case names cannot start with digits. In such situation prefix the + // case with the name of enumeration so it is transformed into valid `SwiftEnum.Case`. + var labelValue = value + if let first = value.first?.unicodeScalars.first, CharacterSet.decimalDigits.contains(first) { + labelValue = "\(jsonEnumeration.name)\(value)" + } + + return SwiftEnum.Case(label: labelValue, rawValue: .string(value: value)) case .integer(let value): + // In Swift, enum case names cannot start with digits, so prefix the case with the name + // of enumeration so it is transformed into valid `SwiftEnum.Case`. return SwiftEnum.Case(label: "\(jsonEnumeration.name)\(value)", rawValue: .integer(value: value)) } }, diff --git a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift index 442255718a..7e183c5a06 100644 --- a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift @@ -158,6 +158,114 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { XCTAssertEqual(expected, actual[0]) } + func testTransformingJSONObjectWithStringEnumerationIntoSwiftStruct() throws { + let object = JSONObject( + name: "Container", + comment: nil, + properties: [ + JSONObject.Property( + name: "enumeration", + comment: nil, + type: JSONEnumeration( + name: "Foo", + comment: "Description of Foo", + values: [ + .string(value: "case1"), + .string(value: "case2"), + .string(value: "3case"), // case name starting with number + .string(value: "4case"), + ] + ), + defaultValue: nil, + isRequired: false, + isReadOnly: false + ) + ] + ) + + let expected = SwiftStruct( + name: "Container", + properties: [ + SwiftStruct.Property( + name: "enumeration", + type: SwiftEnum( + name: "Foo", + comment: "Description of Foo", + cases: [ + SwiftEnum.Case(label: "case1", rawValue: .string(value: "case1")), + SwiftEnum.Case(label: "case2", rawValue: .string(value: "case2")), + SwiftEnum.Case(label: "Foo3case", rawValue: .string(value: "3case")), + SwiftEnum.Case(label: "Foo4case", rawValue: .string(value: "4case")), + ], + conformance: [] + ), + isOptional: true, + mutability: .mutable, + codingKey: .static(value: "enumeration") + ) + ], + conformance: [] + ) + + let actual = try JSONToSwiftTypeTransformer().transform(jsonType: object) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(expected, actual[0]) + } + + func testTransformingJSONObjectWithIntegerEnumerationIntoSwiftStruct() throws { + let object = JSONObject( + name: "Container", + comment: nil, + properties: [ + JSONObject.Property( + name: "enumeration", + comment: nil, + type: JSONEnumeration( + name: "Foo", + comment: "Description of Foo", + values: [ + .integer(value: 1), + .integer(value: 2), + .integer(value: 3), + ] + ), + defaultValue: nil, + isRequired: false, + isReadOnly: false + ) + ] + ) + + let expected = SwiftStruct( + name: "Container", + properties: [ + SwiftStruct.Property( + name: "enumeration", + type: SwiftEnum( + name: "Foo", + comment: "Description of Foo", + cases: [ + SwiftEnum.Case(label: "Foo1", rawValue: .integer(value: 1)), + SwiftEnum.Case(label: "Foo2", rawValue: .integer(value: 2)), + SwiftEnum.Case(label: "Foo3", rawValue: .integer(value: 3)), + ], + conformance: [] + ), + isOptional: true, + mutability: .mutable, + codingKey: .static(value: "enumeration") + ) + ], + conformance: [] + ) + + let actual = try JSONToSwiftTypeTransformer().transform(jsonType: object) + + XCTAssertEqual(actual.count, 1) + XCTAssertEqual(expected, actual[0]) + } + // MARK: - Transforming `additionalProperties` func testTransformingNestedJSONObjectWithIntAdditionalPropertiesIntoSwiftDictionaryInsideRootStruct() throws { From 5b060f9a88ad8ea56666e16ab31ef78c7821fbaf Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 22 Jan 2024 12:51:41 +0100 Subject: [PATCH 46/60] [RUM models generation] Fix optionality for enum cases named as "none" as it was causing compiler ambiguity with `Optional.none`. --- .../Transformers/Swift/JSONToSwiftTypeTransformer.swift | 8 +++++++- .../Transformers/JSONToSwiftTypeTransformerTests.swift | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift index 84b6adb1b6..c2eb817ba9 100644 --- a/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift +++ b/tools/rum-models-generator/Sources/CodeGeneration/Generate/Transformers/Swift/JSONToSwiftTypeTransformer.swift @@ -96,7 +96,13 @@ internal class JSONToSwiftTypeTransformer { // case with the name of enumeration so it is transformed into valid `SwiftEnum.Case`. var labelValue = value if let first = value.first?.unicodeScalars.first, CharacterSet.decimalDigits.contains(first) { - labelValue = "\(jsonEnumeration.name)\(value)" + labelValue = "\(jsonEnumeration.name.lowercasingFirst)\(value)" + } + // In Swift, naming case a "none" might clash with `Optional.none` type if the property of + // this enum value is declared as optional. For that reason, prefix "none" case with the + // name of enumeration to avoid compiler ambiguity. + if labelValue == "none" { + labelValue = "\(jsonEnumeration.name.lowercasingFirst)\(labelValue.uppercasingFirst)" } return SwiftEnum.Case(label: labelValue, rawValue: .string(value: value)) diff --git a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift index 7e183c5a06..8ade6d58e6 100644 --- a/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift +++ b/tools/rum-models-generator/Tests/CodeGenerationTests/Generate/Transformers/JSONToSwiftTypeTransformerTests.swift @@ -173,7 +173,7 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { .string(value: "case1"), .string(value: "case2"), .string(value: "3case"), // case name starting with number - .string(value: "4case"), + .string(value: "none"), // explicit case named as "none" ] ), defaultValue: nil, @@ -194,8 +194,8 @@ final class JSONToSwiftTypeTransformerTests: XCTestCase { cases: [ SwiftEnum.Case(label: "case1", rawValue: .string(value: "case1")), SwiftEnum.Case(label: "case2", rawValue: .string(value: "case2")), - SwiftEnum.Case(label: "Foo3case", rawValue: .string(value: "3case")), - SwiftEnum.Case(label: "Foo4case", rawValue: .string(value: "4case")), + SwiftEnum.Case(label: "foo3case", rawValue: .string(value: "3case")), + SwiftEnum.Case(label: "fooNone", rawValue: .string(value: "none")), ], conformance: [] ), From ff78c4835a597a59a6df043d2743d1e8224983f2 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 22 Jan 2024 13:00:44 +0100 Subject: [PATCH 47/60] Update RUM models (`make rum-models-generate`) --- .../Sources/RUM/RUMDataModels+objc.swift | 332 ++++++++++++++---- .../Sources/DataModels/RUMDataModels.swift | 18 +- 2 files changed, 271 insertions(+), 79 deletions(-) diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 05b84a218b..b18e9c6268 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -510,8 +510,12 @@ public class DDRUMActionEventRUMConnectivity: NSObject { root.swiftModel.connectivity!.cellular != nil ? DDRUMActionEventRUMConnectivityCellular(root: root) : nil } - @objc public var interfaces: [Int] { - root.swiftModel.connectivity!.interfaces.map { DDRUMActionEventRUMConnectivityInterfaces(swift: $0).rawValue } + @objc public var effectiveType: DDRUMActionEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMActionEventRUMConnectivityInterfaces(swift: $0).rawValue } } @objc public var status: DDRUMActionEventRUMConnectivityStatus { @@ -536,24 +540,55 @@ public class DDRUMActionEventRUMConnectivityCellular: NSObject { } } +@objc +public enum DDRUMActionEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + @objc public enum DDRUMActionEventRUMConnectivityInterfaces: Int { - internal init(swift: RUMConnectivity.Interfaces) { + internal init(swift: RUMConnectivity.Interfaces?) { switch swift { - case .bluetooth: self = .bluetooth - case .cellular: self = .cellular - case .ethernet: self = .ethernet - case .wifi: self = .wifi - case .wimax: self = .wimax - case .mixed: self = .mixed - case .other: self = .other - case .unknown: self = .unknown - case .none: self = .none + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone } } - internal var toSwift: RUMConnectivity.Interfaces { + internal var toSwift: RUMConnectivity.Interfaces? { switch self { + case .none: return nil case .bluetooth: return .bluetooth case .cellular: return .cellular case .ethernet: return .ethernet @@ -562,10 +597,11 @@ public enum DDRUMActionEventRUMConnectivityInterfaces: Int { case .mixed: return .mixed case .other: return .other case .unknown: return .unknown - case .none: return .none + case .interfacesNone: return .interfacesNone } } + case none case bluetooth case cellular case ethernet @@ -574,7 +610,7 @@ public enum DDRUMActionEventRUMConnectivityInterfaces: Int { case mixed case other case unknown - case none + case interfacesNone } @objc @@ -1253,8 +1289,12 @@ public class DDRUMErrorEventRUMConnectivity: NSObject { root.swiftModel.connectivity!.cellular != nil ? DDRUMErrorEventRUMConnectivityCellular(root: root) : nil } - @objc public var interfaces: [Int] { - root.swiftModel.connectivity!.interfaces.map { DDRUMErrorEventRUMConnectivityInterfaces(swift: $0).rawValue } + @objc public var effectiveType: DDRUMErrorEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMErrorEventRUMConnectivityInterfaces(swift: $0).rawValue } } @objc public var status: DDRUMErrorEventRUMConnectivityStatus { @@ -1279,24 +1319,55 @@ public class DDRUMErrorEventRUMConnectivityCellular: NSObject { } } +@objc +public enum DDRUMErrorEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + @objc public enum DDRUMErrorEventRUMConnectivityInterfaces: Int { - internal init(swift: RUMConnectivity.Interfaces) { + internal init(swift: RUMConnectivity.Interfaces?) { switch swift { - case .bluetooth: self = .bluetooth - case .cellular: self = .cellular - case .ethernet: self = .ethernet - case .wifi: self = .wifi - case .wimax: self = .wimax - case .mixed: self = .mixed - case .other: self = .other - case .unknown: self = .unknown - case .none: self = .none + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone } } - internal var toSwift: RUMConnectivity.Interfaces { + internal var toSwift: RUMConnectivity.Interfaces? { switch self { + case .none: return nil case .bluetooth: return .bluetooth case .cellular: return .cellular case .ethernet: return .ethernet @@ -1305,10 +1376,11 @@ public enum DDRUMErrorEventRUMConnectivityInterfaces: Int { case .mixed: return .mixed case .other: return .other case .unknown: return .unknown - case .none: return .none + case .interfacesNone: return .interfacesNone } } + case none case bluetooth case cellular case ethernet @@ -1317,7 +1389,7 @@ public enum DDRUMErrorEventRUMConnectivityInterfaces: Int { case mixed case other case unknown - case none + case interfacesNone } @objc @@ -2370,8 +2442,12 @@ public class DDRUMLongTaskEventRUMConnectivity: NSObject { root.swiftModel.connectivity!.cellular != nil ? DDRUMLongTaskEventRUMConnectivityCellular(root: root) : nil } - @objc public var interfaces: [Int] { - root.swiftModel.connectivity!.interfaces.map { DDRUMLongTaskEventRUMConnectivityInterfaces(swift: $0).rawValue } + @objc public var effectiveType: DDRUMLongTaskEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMLongTaskEventRUMConnectivityInterfaces(swift: $0).rawValue } } @objc public var status: DDRUMLongTaskEventRUMConnectivityStatus { @@ -2396,24 +2472,55 @@ public class DDRUMLongTaskEventRUMConnectivityCellular: NSObject { } } +@objc +public enum DDRUMLongTaskEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + @objc public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int { - internal init(swift: RUMConnectivity.Interfaces) { + internal init(swift: RUMConnectivity.Interfaces?) { switch swift { - case .bluetooth: self = .bluetooth - case .cellular: self = .cellular - case .ethernet: self = .ethernet - case .wifi: self = .wifi - case .wimax: self = .wimax - case .mixed: self = .mixed - case .other: self = .other - case .unknown: self = .unknown - case .none: self = .none + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone } } - internal var toSwift: RUMConnectivity.Interfaces { + internal var toSwift: RUMConnectivity.Interfaces? { switch self { + case .none: return nil case .bluetooth: return .bluetooth case .cellular: return .cellular case .ethernet: return .ethernet @@ -2422,10 +2529,11 @@ public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int { case .mixed: return .mixed case .other: return .other case .unknown: return .unknown - case .none: return .none + case .interfacesNone: return .interfacesNone } } + case none case bluetooth case cellular case ethernet @@ -2434,7 +2542,7 @@ public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int { case mixed case other case unknown - case none + case interfacesNone } @objc @@ -3142,8 +3250,12 @@ public class DDRUMResourceEventRUMConnectivity: NSObject { root.swiftModel.connectivity!.cellular != nil ? DDRUMResourceEventRUMConnectivityCellular(root: root) : nil } - @objc public var interfaces: [Int] { - root.swiftModel.connectivity!.interfaces.map { DDRUMResourceEventRUMConnectivityInterfaces(swift: $0).rawValue } + @objc public var effectiveType: DDRUMResourceEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMResourceEventRUMConnectivityInterfaces(swift: $0).rawValue } } @objc public var status: DDRUMResourceEventRUMConnectivityStatus { @@ -3168,24 +3280,55 @@ public class DDRUMResourceEventRUMConnectivityCellular: NSObject { } } +@objc +public enum DDRUMResourceEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + @objc public enum DDRUMResourceEventRUMConnectivityInterfaces: Int { - internal init(swift: RUMConnectivity.Interfaces) { + internal init(swift: RUMConnectivity.Interfaces?) { switch swift { - case .bluetooth: self = .bluetooth - case .cellular: self = .cellular - case .ethernet: self = .ethernet - case .wifi: self = .wifi - case .wimax: self = .wimax - case .mixed: self = .mixed - case .other: self = .other - case .unknown: self = .unknown - case .none: self = .none + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone } } - internal var toSwift: RUMConnectivity.Interfaces { + internal var toSwift: RUMConnectivity.Interfaces? { switch self { + case .none: return nil case .bluetooth: return .bluetooth case .cellular: return .cellular case .ethernet: return .ethernet @@ -3194,10 +3337,11 @@ public enum DDRUMResourceEventRUMConnectivityInterfaces: Int { case .mixed: return .mixed case .other: return .other case .unknown: return .unknown - case .none: return .none + case .interfacesNone: return .interfacesNone } } + case none case bluetooth case cellular case ethernet @@ -3206,7 +3350,7 @@ public enum DDRUMResourceEventRUMConnectivityInterfaces: Int { case mixed case other case unknown - case none + case interfacesNone } @objc @@ -4309,8 +4453,12 @@ public class DDRUMViewEventRUMConnectivity: NSObject { root.swiftModel.connectivity!.cellular != nil ? DDRUMViewEventRUMConnectivityCellular(root: root) : nil } - @objc public var interfaces: [Int] { - root.swiftModel.connectivity!.interfaces.map { DDRUMViewEventRUMConnectivityInterfaces(swift: $0).rawValue } + @objc public var effectiveType: DDRUMViewEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMViewEventRUMConnectivityInterfaces(swift: $0).rawValue } } @objc public var status: DDRUMViewEventRUMConnectivityStatus { @@ -4335,24 +4483,55 @@ public class DDRUMViewEventRUMConnectivityCellular: NSObject { } } +@objc +public enum DDRUMViewEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + @objc public enum DDRUMViewEventRUMConnectivityInterfaces: Int { - internal init(swift: RUMConnectivity.Interfaces) { + internal init(swift: RUMConnectivity.Interfaces?) { switch swift { - case .bluetooth: self = .bluetooth - case .cellular: self = .cellular - case .ethernet: self = .ethernet - case .wifi: self = .wifi - case .wimax: self = .wimax - case .mixed: self = .mixed - case .other: self = .other - case .unknown: self = .unknown - case .none: self = .none + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone } } - internal var toSwift: RUMConnectivity.Interfaces { + internal var toSwift: RUMConnectivity.Interfaces? { switch self { + case .none: return nil case .bluetooth: return .bluetooth case .cellular: return .cellular case .ethernet: return .ethernet @@ -4361,10 +4540,11 @@ public enum DDRUMViewEventRUMConnectivityInterfaces: Int { case .mixed: return .mixed case .other: return .other case .unknown: return .unknown - case .none: return .none + case .interfacesNone: return .interfacesNone } } + case none case bluetooth case cellular case ethernet @@ -4373,7 +4553,7 @@ public enum DDRUMViewEventRUMConnectivityInterfaces: Int { case mixed case other case unknown - case none + case interfacesNone } @objc @@ -6123,4 +6303,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/83f8760b46e9a117b5975cfb592b1803d643ee3e +// Generated from https://github.com/DataDog/rum-events-format/tree/389581be98dcf8efbfcfe7bffaa32d53f960fb6f diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index a1b747f1ed..c499e6e7a8 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -3287,14 +3287,18 @@ public struct RUMConnectivity: Codable { /// Cellular connectivity properties public let cellular: Cellular? + /// Cellular connection type reflecting the measured network performance + public let effectiveType: EffectiveType? + /// The list of available network interfaces - public let interfaces: [Interfaces] + public let interfaces: [Interfaces]? /// Status of the device connectivity public let status: Status enum CodingKeys: String, CodingKey { case cellular = "cellular" + case effectiveType = "effective_type" case interfaces = "interfaces" case status = "status" } @@ -3313,6 +3317,14 @@ public struct RUMConnectivity: Codable { } } + /// Cellular connection type reflecting the measured network performance + public enum EffectiveType: String, Codable { + case slow2g = "slow_2g" + case effectiveType2g = "2g" + case effectiveType3g = "3g" + case effectiveType4g = "4g" + } + public enum Interfaces: String, Codable { case bluetooth = "bluetooth" case cellular = "cellular" @@ -3322,7 +3334,7 @@ public struct RUMConnectivity: Codable { case mixed = "mixed" case other = "other" case unknown = "unknown" - case none = "none" + case interfacesNone = "none" } /// Status of the device connectivity @@ -3555,4 +3567,4 @@ public enum RUMMethod: String, Codable { case patch = "PATCH" } -// Generated from https://github.com/DataDog/rum-events-format/tree/83f8760b46e9a117b5975cfb592b1803d643ee3e +// Generated from https://github.com/DataDog/rum-events-format/tree/389581be98dcf8efbfcfe7bffaa32d53f960fb6f From 9334f28c503e2a931b6d59a37b420d6ba55a9beb Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 22 Jan 2024 13:02:44 +0100 Subject: [PATCH 48/60] Make RUM compile for updated RUM models --- DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift | 1 + DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift | 1 + DatadogRUM/Sources/RUMEvent/RUMConnectivityInfoProvider.swift | 3 ++- DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 4f77e9b098..af2928cb53 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -26,6 +26,7 @@ extension RUMConnectivity { carrierName: .mockRandom(), technology: .mockRandom() ), + effectiveType: nil, interfaces: [.bluetooth, .cellular].randomElements(), status: [.connected, .maybe, .notConnected].randomElement()! ) diff --git a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift index 9dcf403544..e10170d75e 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift @@ -690,6 +690,7 @@ class RUMMonitorTests: XCTestCase { let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() let expectedConnectivityInfo = RUMConnectivity( cellular: RUMConnectivity.Cellular(carrierName: "Carrier Name", technology: "GPRS"), + effectiveType: nil, interfaces: [.cellular], status: .connected ) diff --git a/DatadogRUM/Sources/RUMEvent/RUMConnectivityInfoProvider.swift b/DatadogRUM/Sources/RUMEvent/RUMConnectivityInfoProvider.swift index 8eb7887a96..4a6c980363 100644 --- a/DatadogRUM/Sources/RUMEvent/RUMConnectivityInfoProvider.swift +++ b/DatadogRUM/Sources/RUMEvent/RUMConnectivityInfoProvider.swift @@ -22,6 +22,7 @@ extension RUMConnectivity { self.init( cellular: carrierInfo.flatMap { RUMConnectivity.connectivityCellularInfo(for: $0) }, + effectiveType: nil, interfaces: RUMConnectivity.connectivityInterfaces(for: networkInfo), status: RUMConnectivity.connectivityStatus(for: networkInfo) ) @@ -39,7 +40,7 @@ extension RUMConnectivity { private static func connectivityInterfaces(for networkInfo: NetworkConnectionInfo) -> [RUMConnectivity.Interfaces] { guard let availableInterfaces = networkInfo.availableInterfaces, !availableInterfaces.isEmpty else { - return [.none] + return [.interfacesNone] } return availableInterfaces.map { interface in diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index ab2dea5e77..a523f86be6 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -39,6 +39,7 @@ extension RUMConnectivity: RandomMockable { carrierName: .mockRandom(), technology: .mockRandom() ), + effectiveType: nil, interfaces: [.bluetooth, .cellular].randomElements(), status: [.connected, .maybe, .notConnected].randomElement()! ) From 703b6ecb9108c48597f2cdd5522e9b3f62b95707 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 18 Jan 2024 19:20:55 +0100 Subject: [PATCH 49/60] RUM-2690 Fix URLSession instrumentation for iOS 13.x and iOS 12.x --- .../URLSession/URLSessionInstrumentation.swift | 8 ++++++++ .../URLSession/URLSessionSwizzler.swift | 10 ++++++++-- .../URLSession/URLSessionTaskSwizzler.swift | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift index faa5e86e02..02bf907129 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift @@ -18,6 +18,10 @@ public enum URLSessionInstrumentation { try enableOrThrow(with: configuration, in: core) } catch let error { consolePrint("\(error)", .error) + + if error is InternalError { // SDK error, send to telemetry + core.telemetry.error(error) + } } } @@ -38,6 +42,10 @@ public enum URLSessionInstrumentation { try disableOrThrow(delegateClass: delegateClass, in: core) } catch let error { consolePrint("\(error)", .error) + + if error is InternalError { // SDK error, send to telemetry + core.telemetry.error(error) + } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift index 4758689131..2143952fee 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift @@ -24,8 +24,14 @@ internal final class URLSessionSwizzler { defer { lock.unlock() } dataTaskURLRequestCompletionHandler = try DataTaskURLRequestCompletionHandler.build() dataTaskURLRequestCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler) - dataTaskURLCompletionHandler = try DataTaskURLCompletionHandler.build() - dataTaskURLCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler) + + if #available(iOS 13.0, *) { + // Prior to iOS 13.0 the `URLSession.dataTask(with:url, completionHandler:handler)` makes an internal + // call to `URLSession.dataTask(with:request, completionHandler:handler)`. To avoid duplicated call + // to the callback, we don't apply below swizzling prior to iOS 13. + dataTaskURLCompletionHandler = try DataTaskURLCompletionHandler.build() + dataTaskURLCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler) + } } /// Unswizzles all. diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift index 68ad2ef713..7b20b41381 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift @@ -44,7 +44,13 @@ internal final class URLSessionTaskSwizzler { private let method: Method static func build() throws -> TaskResume { - return try TaskResume(selector: self.selector, klass: URLSessionTask.self) + // RUM-2690: We swizzle private `__NSCFLocalSessionTask` class as it appears to be uniformly used + // in iOS versions 12.x - 17.x. Swizzling the public `URLSessionTask.resume()` doesn't work in 12.x and 13.x. + let className = "__NSCFLocalSessionTask" + guard let klass = NSClassFromString(className) else { + throw InternalError(description: "Failed to swizzle `URLSessionTask`: `\(className)` class not found.") + } + return try TaskResume(selector: self.selector, klass: klass) } private init(selector: Selector, klass: AnyClass) throws { From 6cbade18e1bbc93c955dfcbee96cd5ce70b743b1 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 18 Jan 2024 19:27:03 +0100 Subject: [PATCH 50/60] RUM-2690 Update CHANGELOG.md --- CHANGELOG.md | 2 ++ .../URLSession/URLSessionTaskSwizzler.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c3dc15ec..5526380b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - [FIX] RUM session not being linked to spans. See [#1615][] +- [FIX] `URLSessionTask.resume()` swizzling in iOS 13 and 12. See [#1637][] - [FEATURE] Allow stopping a core instance. See [#1541][] # 2.6.0 / 09-01-2024 @@ -576,6 +577,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609 [#1615]: https://github.com/DataDog/dd-sdk-ios/pull/1615 [#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 +[#1637]: https://github.com/DataDog/dd-sdk-ios/pull/1637 [#1541]: https://github.com/DataDog/dd-sdk-ios/pull/1541 [#1596]: https://github.com/DataDog/dd-sdk-ios/pull/1596 [#1597]: https://github.com/DataDog/dd-sdk-ios/pull/1597 diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift index 7b20b41381..2e84507fac 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift @@ -46,6 +46,7 @@ internal final class URLSessionTaskSwizzler { static func build() throws -> TaskResume { // RUM-2690: We swizzle private `__NSCFLocalSessionTask` class as it appears to be uniformly used // in iOS versions 12.x - 17.x. Swizzling the public `URLSessionTask.resume()` doesn't work in 12.x and 13.x. + // See https://github.com/DataDog/dd-sdk-ios/pull/1637 for full `URLSessionTask` class dumps in major iOS versions. let className = "__NSCFLocalSessionTask" guard let klass = NSClassFromString(className) else { throw InternalError(description: "Failed to swizzle `URLSessionTask`: `\(className)` class not found.") From 786d50623981e0d9397ad0bc2fae8294a58952d7 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 18 Jan 2024 13:35:38 +0100 Subject: [PATCH 51/60] RUM-2690 Add debug flow for testing instrumented requests prior to iOS 13 --- .../Example/Base.lproj/Main iOS.storyboard | 285 +++++++++--------- .../Debugging/DebugRUMViewController.swift | 62 +++- 2 files changed, 196 insertions(+), 151 deletions(-) diff --git a/Datadog/Example/Base.lproj/Main iOS.storyboard b/Datadog/Example/Base.lproj/Main iOS.storyboard index 45f77d1e5e..d4a0c551e4 100644 --- a/Datadog/Example/Base.lproj/Main iOS.storyboard +++ b/Datadog/Example/Base.lproj/Main iOS.storyboard @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -126,7 +126,7 @@ - + @@ -146,7 +146,7 @@ - + @@ -166,7 +166,7 @@ - + @@ -204,7 +204,7 @@ - + @@ -224,26 +224,26 @@ - + - + - + @@ -261,33 +261,33 @@ - + - + - + @@ -301,27 +301,27 @@ - + - + - + @@ -329,14 +329,14 @@ - + - +