From d0a731cd131818fe486a3efe49b0630a9de1509b Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 26 Mar 2024 10:46:15 +0000 Subject: [PATCH 01/13] RUM-3569 Use data store to remove resource duplicates between sessions --- .../Sources/Writers/ResourcesWriter.swift | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index a2110440d5..656dfdc64d 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -16,25 +16,55 @@ internal protocol ResourcesWriting { internal class ResourcesWriter: ResourcesWriting { /// An instance of SDK core the SR feature is registered to. - private weak var core: DatadogCoreProtocol? + private let scope: FeatureScope? + private let telemetry: Telemetry + + @ReadWriteLock + private var knownIdentifiers = Set() { + didSet { + if let knownIdentifiers = try? JSONEncoder().encode(knownIdentifiers) { + scope?.dataStore.setValue( + knownIdentifiers, + forKey: Constants.processedResourcesKey + ) + } + } + } init( core: DatadogCoreProtocol ) { - self.core = core + self.scope = core.scope(for: ResourcesFeature.name) + self.telemetry = core.telemetry + + self.scope?.dataStore.value(forKey: Constants.processedResourcesKey) { [weak self] result in + switch result { + case .value(let data, _): + if let knownIdentifiers = try? JSONDecoder().decode(Set.self, from: data) { + self?.knownIdentifiers.formUnion(knownIdentifiers) + } + case .error(let error): + self?.telemetry.error("Failed to read processed resources from data store: \(error)") + case .noValue: + break + } + } } // MARK: - Writing func write(resources: [EnrichedResource]) { - guard let scope = core?.scope(for: ResourcesFeature.self) else { - return - } - scope.eventWriteContext { _, recordWriter in - resources.forEach { - recordWriter.write(value: $0) + scope?.eventWriteContext { [weak self] _, recordWriter in + let unknownResources = resources.filter { self?.knownIdentifiers.contains($0.identifier) == false } + for resource in unknownResources { + recordWriter.write(value: resource) } + self?.knownIdentifiers.formUnion(Set(unknownResources.map { $0.identifier })) } } + + private enum Constants { + static let processedResourcesKey = "processed-resources" + } } #endif From 2aa2584325130a9a42f40ac1c0e5b3470bb55e45 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 26 Mar 2024 11:43:10 +0000 Subject: [PATCH 02/13] RUM-3569 Add tests --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 ++++ .../DatadogSessionReplay iOS.xcscheme | 4 ++- .../Tests/Mocks/ResourceMocks.swift | 12 +++++++ .../Tests/Writer/ResourcesWriterTests.swift | 32 +++++++++++++++++++ .../Mocks/CoreMocks/PassthroughCoreMock.swift | 4 ++- .../CoreMocks/SingleFeatureCoreMock.swift | 4 +++ 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index f9ad1abc2d..8ba4f4bc88 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -552,6 +552,8 @@ 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9EE5AD8226205B82001E699E /* DDNSURLSessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */; }; + A703694D2BB2E30100C66C36 /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703694C2BB2E30100C66C36 /* DataStoreMock.swift */; }; + A703694E2BB2E30100C66C36 /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703694C2BB2E30100C66C36 /* DataStoreMock.swift */; }; A70A82652A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A70ADCD22B583B1300321BC9 /* UIImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70ADCD12B583B1300321BC9 /* UIImageResource.swift */; }; @@ -2519,6 +2521,7 @@ 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReader.swift; sourceTree = ""; }; 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReaderTests.swift; sourceTree = ""; }; 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNSURLSessionDelegateTests.swift; sourceTree = ""; }; + A703694C2BB2E30100C66C36 /* DataStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreMock.swift; sourceTree = ""; }; A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskCoordinator.swift; sourceTree = ""; }; A70ADCD12B583B1300321BC9 /* UIImageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageResource.swift; sourceTree = ""; }; A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriterTests.swift; sourceTree = ""; }; @@ -5659,6 +5662,7 @@ 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */, 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */, + A703694C2BB2E30100C66C36 /* DataStoreMock.swift */, ); path = Mocks; sourceTree = ""; @@ -8399,6 +8403,7 @@ buildActionMask = 2147483647; files = ( 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, + A703694D2BB2E30100C66C36 /* DataStoreMock.swift in Sources */, D2160CF229C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, D257955B298ABB04008A1BE5 /* XCTestCase.swift in Sources */, D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */, @@ -8444,6 +8449,7 @@ buildActionMask = 2147483647; files = ( 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, + A703694E2BB2E30100C66C36 /* DataStoreMock.swift in Sources */, D2160CF329C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, D2579578298ABB83008A1BE5 /* XCTestCase.swift in Sources */, D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme index 440cbbab98..8d567bb90c 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme @@ -26,7 +26,9 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + enableThreadSanitizer = "YES" + disableMainThreadChecker = "YES"> EnrichedResource { + return .init( + identifier: identifier, + data: data, + context: context + ) + } + public static func mockRandom() -> Self { return .init( identifier: .mockRandom(), diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index 16eb97e9ec..a5bdbed39d 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -39,4 +39,36 @@ class ResourcesWriterTests: XCTestCase { XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) } + + func testWhenFeatureScopeIsConnected_itWritesResourcesToCore_andRemovesDuplicates() throws { + // Given + let dataStore = DataStoreMock() + let core = PassthroughCoreMock(dataStore: dataStore) + + // When + let writer = ResourcesWriter(core: core) + + // Then + writer.write(resources: [.mockWith(identifier: "1")]) + writer.write(resources: [.mockWith(identifier: "1")]) + + XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 1) + let data = try XCTUnwrap(dataStore.values["processed-resources"]) + XCTAssertGreaterThan(data.count, 0) + } + + func testWhenFeatureScopeIsConnected_itWritesResourcesToCore_andReadsKnownDuplicates() throws { + // Given + let dataStore = DataStoreMock() + dataStore.values["processed-resources"] = try JSONEncoder().encode(Set(["1"])) + let core = PassthroughCoreMock(dataStore: dataStore) + + // When + let writer = ResourcesWriter(core: core) + + // Then + writer.write(resources: [.mockWith(identifier: "1")]) + + XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) + } } diff --git a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift index 5ff9a11067..27ffb9b0e7 100644 --- a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift +++ b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift @@ -62,11 +62,13 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { public required init( context: DatadogContext = .mockAny(), + dataStore: DataStore = NOPDataStore(), expectation: XCTestExpectation? = nil, bypassConsentExpectation: XCTestExpectation? = nil, messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() ) { self.context = context + self.dataStore = dataStore self.expectation = expectation self.bypassConsentExpectation = bypassConsentExpectation self.messageReceiver = messageReceiver @@ -116,7 +118,7 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { block(context) } - public var dataStore: DataStore { NOPDataStore() } + public var dataStore: DataStore /// Recorded events from feature scopes. /// diff --git a/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift b/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift index fdd1887450..45f87c5e62 100644 --- a/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift +++ b/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift @@ -42,6 +42,7 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea /// is invoked with `bypassConsent` parameter set to `true`. public required init( context: DatadogContext = .mockAny(), + dataStore: DataStore = NOPDataStore(), feature: Feature? = nil, expectation: XCTestExpectation? = nil, bypassConsentExpectation: XCTestExpectation? = nil, @@ -51,6 +52,7 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea super.init( context: context, + dataStore: dataStore, expectation: expectation, bypassConsentExpectation: bypassConsentExpectation, messageReceiver: messageReceiver @@ -67,6 +69,7 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea /// is invoked with `bypassConsent` parameter set to `true`. public required init( context: DatadogContext = .mockAny(), + dataStore: DataStore = NOPDataStore(), expectation: XCTestExpectation? = nil, bypassConsentExpectation: XCTestExpectation? = nil, messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() @@ -75,6 +78,7 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea super.init( context: context, + dataStore: dataStore, expectation: expectation, bypassConsentExpectation: bypassConsentExpectation, messageReceiver: messageReceiver From 178cefd986437930c4414f868b8a83d9dcc55fc5 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 26 Mar 2024 11:56:08 +0000 Subject: [PATCH 03/13] RUM-3569 Register resources before session replay --- DatadogSessionReplay/Sources/SessionReplay.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DatadogSessionReplay/Sources/SessionReplay.swift b/DatadogSessionReplay/Sources/SessionReplay.swift index c0054d6872..a4ec8c9657 100644 --- a/DatadogSessionReplay/Sources/SessionReplay.swift +++ b/DatadogSessionReplay/Sources/SessionReplay.swift @@ -42,12 +42,11 @@ public enum SessionReplay { guard configuration.replaySampleRate > 0 else { return } + let resources = ResourcesFeature(core: core, configuration: configuration) + try core.register(feature: resources) let sessionReplay = try SessionReplayFeature(core: core, configuration: configuration) try core.register(feature: sessionReplay) - - let resources = ResourcesFeature(core: core, configuration: configuration) - try core.register(feature: resources) } } #endif From 40756d6242b726cc9205de937ef507f5aaa160ab Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 26 Mar 2024 13:17:11 +0000 Subject: [PATCH 04/13] RUM-3569 Fix after rebase --- DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index 656dfdc64d..5e32c05d8a 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -34,7 +34,7 @@ internal class ResourcesWriter: ResourcesWriting { init( core: DatadogCoreProtocol ) { - self.scope = core.scope(for: ResourcesFeature.name) + self.scope = core.scope(for: ResourcesFeature.self) self.telemetry = core.telemetry self.scope?.dataStore.value(forKey: Constants.processedResourcesKey) { [weak self] result in From 342386b6441531bd21dc7e9f342287f46d9441dc Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 27 Mar 2024 10:41:06 +0000 Subject: [PATCH 05/13] RUM-3569 Refactor --- .../Sources/Writers/ResourcesWriter.swift | 72 +++++++++++++++---- .../Tests/Writer/ResourcesWriterTests.swift | 44 ++++++++++-- 2 files changed, 95 insertions(+), 21 deletions(-) diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index 5e32c05d8a..5573b9c38e 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -22,31 +22,42 @@ internal class ResourcesWriter: ResourcesWriting { @ReadWriteLock private var knownIdentifiers = Set() { didSet { - if let knownIdentifiers = try? JSONEncoder().encode(knownIdentifiers) { + if let knownIdentifiers = knownIdentifiers.asData() { scope?.dataStore.setValue( knownIdentifiers, - forKey: Constants.processedResourcesKey + forKey: Constants.knownResourcesKey ) } } } init( - core: DatadogCoreProtocol + core: DatadogCoreProtocol, + dataStoreResetTime: TimeInterval = TimeInterval(30).days ) { self.scope = core.scope(for: ResourcesFeature.self) self.telemetry = core.telemetry - self.scope?.dataStore.value(forKey: Constants.processedResourcesKey) { [weak self] result in - switch result { - case .value(let data, _): - if let knownIdentifiers = try? JSONDecoder().decode(Set.self, from: data) { - self?.knownIdentifiers.formUnion(knownIdentifiers) + self.scope?.dataStore.value(forKey: Constants.storeCreationKey) { result in + if let storeCreation = result.data()?.asTimeInterval(), Date().timeIntervalSince1970 - storeCreation < dataStoreResetTime { + self.scope?.dataStore.value(forKey: Constants.knownResourcesKey) { [weak self] result in + switch result { + case .value(let data, _): + if let knownIdentifiers = data.asKnownIdentifiers() { + self?.knownIdentifiers.formUnion(knownIdentifiers) + } + case .error(let error): + self?.telemetry.error("Failed to read processed resources from data store: \(error)") + case .noValue: + break + } } - case .error(let error): - self?.telemetry.error("Failed to read processed resources from data store: \(error)") - case .noValue: - break + } else { // Reset if store was created more than 30 days ago + self.scope?.dataStore.setValue( + Date().timeIntervalSince1970.asData(), + forKey: Constants.storeCreationKey + ) + self.scope?.dataStore.removeValue(forKey: Constants.knownResourcesKey) } } } @@ -57,14 +68,47 @@ internal class ResourcesWriter: ResourcesWriting { scope?.eventWriteContext { [weak self] _, recordWriter in let unknownResources = resources.filter { self?.knownIdentifiers.contains($0.identifier) == false } for resource in unknownResources { + print("👾 Resource ID: ", resource.identifier) recordWriter.write(value: resource) } self?.knownIdentifiers.formUnion(Set(unknownResources.map { $0.identifier })) } } - private enum Constants { - static let processedResourcesKey = "processed-resources" + enum Constants { + static let knownResourcesKey = "known-resources" + static let storeCreationKey = "store-creation" + } +} + +extension Data { + func asTimeInterval() -> TimeInterval? { + var value: TimeInterval = 0 + guard count >= MemoryLayout.size(ofValue: value) else { + return nil + } + _ = Swift.withUnsafeMutableBytes(of: &value) { + copyBytes(to: $0) + } + return value + } + + func asKnownIdentifiers() -> Set? { + return try? JSONDecoder().decode(Set.self, from: self) + } +} + +extension TimeInterval { + func asData() -> Data { + return Swift.withUnsafeBytes(of: self) { + Data($0) + } + } +} + +extension Set { + func asData() -> Data? { + return try? JSONEncoder().encode(self) } } #endif diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index a5bdbed39d..6702bf5b60 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -40,27 +40,28 @@ class ResourcesWriterTests: XCTestCase { XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) } - func testWhenFeatureScopeIsConnected_itWritesResourcesToCore_andRemovesDuplicates() throws { + func testWritesSameResourcesToCore_andRemovesDuplicates() throws { // Given let dataStore = DataStoreMock() + dataStore.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() let core = PassthroughCoreMock(dataStore: dataStore) // When let writer = ResourcesWriter(core: core) - - // Then writer.write(resources: [.mockWith(identifier: "1")]) writer.write(resources: [.mockWith(identifier: "1")]) + // Then XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 1) - let data = try XCTUnwrap(dataStore.values["processed-resources"]) + let data = try XCTUnwrap(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) XCTAssertGreaterThan(data.count, 0) } - func testWhenFeatureScopeIsConnected_itWritesResourcesToCore_andReadsKnownDuplicates() throws { + func testWhenReadsKnownDuplicates_itDoesNotWriteRecordsToCore() throws { // Given let dataStore = DataStoreMock() - dataStore.values["processed-resources"] = try JSONEncoder().encode(Set(["1"])) + dataStore.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["1"]).asData() + dataStore.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() let core = PassthroughCoreMock(dataStore: dataStore) // When @@ -68,7 +69,36 @@ class ResourcesWriterTests: XCTestCase { // Then writer.write(resources: [.mockWith(identifier: "1")]) - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) } + + func testWhenDataStoreIsOlderThan30Days_itClearsKnownDuplicates() throws { + // Given + let dataStore = DataStoreMock() + dataStore.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["2", "1"]).asData() + dataStore.values[ResourcesWriter.Constants.storeCreationKey] = (Date().timeIntervalSince1970 - 31.days).asData() + let core = PassthroughCoreMock(dataStore: dataStore) + + // When + let writer = ResourcesWriter(core: core) + XCTAssertNil(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) + + // Then + writer.write(resources: [.mockWith(identifier: "1")]) + XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 1) + XCTAssertEqual(dataStore.values[ResourcesWriter.Constants.knownResourcesKey], Set(["1"]).asData()) + } + + func testWhenInitialized_itSetsUpDataStore() { + // Given + let dataStore = DataStoreMock() + let core = PassthroughCoreMock(dataStore: dataStore) + + // When + _ = ResourcesWriter(core: core) + + // Then + XCTAssertNotNil(dataStore.values[ResourcesWriter.Constants.storeCreationKey]) + XCTAssertNil(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) + } } From 7effbe179814c4dc11eacaaa1b11be0776dd2f86 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Wed, 27 Mar 2024 10:53:40 +0000 Subject: [PATCH 06/13] RUM-3569 PR fixes --- .../xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme | 4 +--- DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme index 8d567bb90c..440cbbab98 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme @@ -26,9 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO" - enableThreadSanitizer = "YES" - disableMainThreadChecker = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO"> Date: Thu, 28 Mar 2024 12:09:55 +0000 Subject: [PATCH 07/13] RUM-3569 Refactor to scope and add telemetry on errors --- .../Feature/SessionReplayFeature.swift | 2 +- .../Sources/Writers/ResourcesWriter.swift | 67 ++++++------ .../Tests/Writer/ResourcesWriterTests.swift | 102 +++++++++--------- 3 files changed, 92 insertions(+), 79 deletions(-) diff --git a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift index 8faf090742..86a9ce45c6 100644 --- a/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift +++ b/DatadogSessionReplay/Sources/Feature/SessionReplayFeature.swift @@ -35,7 +35,7 @@ internal class SessionReplayFeature: DatadogRemoteFeature { ) let resourceProcessor = ResourceProcessor( queue: processorsQueue, - resourcesWriter: ResourcesWriter(core: core) + resourcesWriter: ResourcesWriter(scope: core.scope(for: ResourcesFeature.self)) ) let recorder = try Recorder( snapshotProcessor: snapshotProcessor, diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index 3e789dd3e1..b902bb20df 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -15,15 +15,13 @@ internal protocol ResourcesWriting { } internal class ResourcesWriter: ResourcesWriting { - /// An instance of SDK core the SR feature is registered to. - private let scope: FeatureScope? - private let telemetry: Telemetry + private let scope: FeatureScope @ReadWriteLock private var knownIdentifiers = Set() { didSet { if let knownIdentifiers = knownIdentifiers.asData() { - scope?.dataStore.setValue( + scope.dataStore.setValue( knownIdentifiers, forKey: Constants.knownResourcesKey ) @@ -32,32 +30,37 @@ internal class ResourcesWriter: ResourcesWriting { } init( - core: DatadogCoreProtocol, + scope: FeatureScope, dataStoreResetTime: TimeInterval = TimeInterval(30).days ) { - self.scope = core.scope(for: ResourcesFeature.self) - self.telemetry = core.telemetry + self.scope = scope - self.scope?.dataStore.value(forKey: Constants.storeCreationKey) { result in - if let storeCreation = result.data()?.asTimeInterval(), Date().timeIntervalSince1970 - storeCreation < dataStoreResetTime { - self.scope?.dataStore.value(forKey: Constants.knownResourcesKey) { [weak self] result in - switch result { - case .value(let data, _): - if let knownIdentifiers = data.asKnownIdentifiers() { - self?.knownIdentifiers.formUnion(knownIdentifiers) + self.scope.dataStore.value(forKey: Constants.storeCreationKey) { [weak self] result in + do { + if let storeCreation = try result.data()?.asTimeInterval(), Date().timeIntervalSince1970 - storeCreation < dataStoreResetTime { + self?.scope.dataStore.value(forKey: Constants.knownResourcesKey) { result in + switch result { + case .value(let data, _): + do { + if let knownIdentifiers = try data.asKnownIdentifiers() { + self?.knownIdentifiers.formUnion(knownIdentifiers) + } + } catch let error { + self?.scope.telemetry.error("Failed to decode known identifiers", error: error) + } + default: + break } - case .error(let error): - self?.telemetry.error("Failed to read processed resources from data store: \(error)") - case .noValue: - break } + } else { // Reset if store was created more than 30 days ago + self?.scope.dataStore.setValue( + Date().timeIntervalSince1970.asData(), + forKey: Constants.storeCreationKey + ) + self?.scope.dataStore.removeValue(forKey: Constants.knownResourcesKey) } - } else { // Reset if store was created more than 30 days ago - self.scope?.dataStore.setValue( - Date().timeIntervalSince1970.asData(), - forKey: Constants.storeCreationKey - ) - self.scope?.dataStore.removeValue(forKey: Constants.knownResourcesKey) + } catch let error { + self?.scope.telemetry.error("Failed to decode store creation", error: error) } } } @@ -65,7 +68,7 @@ internal class ResourcesWriter: ResourcesWriting { // MARK: - Writing func write(resources: [EnrichedResource]) { - scope?.eventWriteContext { [weak self] _, recordWriter in + scope.eventWriteContext { [weak self] _, recordWriter in let unknownResources = resources.filter { self?.knownIdentifiers.contains($0.identifier) == false } for resource in unknownResources { recordWriter.write(value: resource) @@ -81,10 +84,14 @@ internal class ResourcesWriter: ResourcesWriting { } extension Data { - func asTimeInterval() -> TimeInterval? { + enum SerializationError: Error { + case invalidData + } + + func asTimeInterval() throws -> TimeInterval { var value: TimeInterval = 0 guard count >= MemoryLayout.size(ofValue: value) else { - return nil + throw SerializationError.invalidData } _ = Swift.withUnsafeMutableBytes(of: &value) { copyBytes(to: $0) @@ -92,8 +99,8 @@ extension Data { return value } - func asKnownIdentifiers() -> Set? { - return try? JSONDecoder().decode(Set.self, from: self) + func asKnownIdentifiers() throws -> Set? { + return try JSONDecoder().decode(Set.self, from: self) } } @@ -107,7 +114,7 @@ extension TimeInterval { extension Set { func asData() -> Data? { - return try? JSONEncoder().encode(self) + return try? JSONEncoder().encode(self) // Never fails } } #endif diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index 6702bf5b60..bcc4a5ca25 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -5,100 +5,106 @@ */ import XCTest +import DatadogInternal @testable import DatadogSessionReplay @testable import TestUtilities class ResourcesWriterTests: XCTestCase { - func testWhenFeatureScopeIsConnected_itWritesResourcesToCore() { - // Given - let core = PassthroughCoreMock() + var scopeMock: FeatureScopeMock! // swiftlint:disable:this implicitly_unwrapped_optional + var writer: ResourcesWriter! // swiftlint:disable:this implicitly_unwrapped_optional - // When - let writer = ResourcesWriter(core: core) + override func setUp() { + scopeMock = FeatureScopeMock() + writer = ResourcesWriter(scope: scopeMock) + } - // Then + override func tearDown() { + writer = nil + scopeMock = nil + } + + func testWhenInitialized_itSetsUpDataStore() { + XCTAssertNotNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey]) + XCTAssertNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) + } + + func test_whenWritesResources_itDoesWriteRecordsToScope() { + // When writer.write(resources: [.mockRandom()]) writer.write(resources: [.mockRandom()]) writer.write(resources: [.mockRandom()]) - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 3) + // Then + XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 3) + XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) } - func testWhenFeatureScopeIsNotConnected_itDoesNotWriteRecordsToCore() throws { + func test_whenWritesSameResourcesToCore_itRemovesDuplicates() throws { // Given - let core = SingleFeatureCoreMock() - let feature = MockFeature() - try core.register(feature: feature) + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() // When - let writer = ResourcesWriter(core: core) + writer.write(resources: [.mockWith(identifier: "1")]) + writer.write(resources: [.mockWith(identifier: "1")]) // Then - writer.write(resources: [.mockRandom()]) - - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) + XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 1) + XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) + let data = try XCTUnwrap(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + XCTAssertGreaterThan(data.count, 0) } - func testWritesSameResourcesToCore_andRemovesDuplicates() throws { + func test_whenReadsKnownDuplicates_itDoesNotWriteRecordsToScope() throws { // Given - let dataStore = DataStoreMock() - dataStore.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() - let core = PassthroughCoreMock(dataStore: dataStore) + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["1"]).asData() + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() + let writer = ResourcesWriter(scope: scopeMock) // When - let writer = ResourcesWriter(core: core) - writer.write(resources: [.mockWith(identifier: "1")]) writer.write(resources: [.mockWith(identifier: "1")]) // Then - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 1) - let data = try XCTUnwrap(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) - XCTAssertGreaterThan(data.count, 0) + XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 0) + XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) } - func testWhenReadsKnownDuplicates_itDoesNotWriteRecordsToCore() throws { + func test_whenDataStoreIsOlderThan30Days_itClearsKnownDuplicates() throws { // Given - let dataStore = DataStoreMock() - dataStore.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["1"]).asData() - dataStore.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() - let core = PassthroughCoreMock(dataStore: dataStore) + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["2", "1"]).asData() + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = (Date().timeIntervalSince1970 - 31.days).asData() + let writer = ResourcesWriter(scope: scopeMock) // When - let writer = ResourcesWriter(core: core) + XCTAssertNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + writer.write(resources: [.mockWith(identifier: "1")]) // Then - writer.write(resources: [.mockWith(identifier: "1")]) - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 0) + XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 1) + XCTAssertEqual(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey], Set(["1"]).asData()) + XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) } - func testWhenDataStoreIsOlderThan30Days_itClearsKnownDuplicates() throws { + func test_whenKnownResourcesAreBroken_itLogsTelemetry() { // Given - let dataStore = DataStoreMock() - dataStore.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["2", "1"]).asData() - dataStore.values[ResourcesWriter.Constants.storeCreationKey] = (Date().timeIntervalSince1970 - 31.days).asData() - let core = PassthroughCoreMock(dataStore: dataStore) + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = "broken".data(using: .utf8) // When - let writer = ResourcesWriter(core: core) - XCTAssertNil(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) + _ = ResourcesWriter(scope: scopeMock) // Then - writer.write(resources: [.mockWith(identifier: "1")]) - XCTAssertEqual(core.events(ofType: EnrichedResource.self).count, 1) - XCTAssertEqual(dataStore.values[ResourcesWriter.Constants.knownResourcesKey], Set(["1"]).asData()) + XCTAssertTrue(scopeMock.telemetryMock.messages[0].asError?.message.contains("Failed to decode known identifiers - ") ?? false) } - func testWhenInitialized_itSetsUpDataStore() { + func test_whenDataStoreCreationIsBroken_itLogsTelemetry() { // Given - let dataStore = DataStoreMock() - let core = PassthroughCoreMock(dataStore: dataStore) + scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = "broken".data(using: .utf8) // When - _ = ResourcesWriter(core: core) + _ = ResourcesWriter(scope: scopeMock) // Then - XCTAssertNotNil(dataStore.values[ResourcesWriter.Constants.storeCreationKey]) - XCTAssertNil(dataStore.values[ResourcesWriter.Constants.knownResourcesKey]) + XCTAssertEqual(scopeMock.telemetryMock.messages[0].asError?.message, "Failed to decode store creation - invalidData") } } From 4a419bccac03acc26e8a70bd440765c6756fe964 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 28 Mar 2024 13:45:17 +0000 Subject: [PATCH 08/13] RUM-3569 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 9ab6d11380..829c935391 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/SnapshotTestCase.swift @@ -49,7 +49,7 @@ internal class SnapshotTestCase: XCTestCase { ) let resourceProcessor = ResourceProcessor( queue: NoQueue(), - resourcesWriter: ResourcesWriter(core: PassthroughCoreMock()) + resourcesWriter: ResourcesWriter(scope: FeatureScopeMock()) ) let recorder = try Recorder( snapshotProcessor: snapshotProcessor, From d743d1856ddfe6a1b608f549f1114f0b0d80451a Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 28 Mar 2024 13:48:52 +0000 Subject: [PATCH 09/13] RUM-3569 Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 584e96a824..04868667e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +- [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] * [FEATURE] Add support for 128 bit trace IDs. See [#1721][] * [FEATURE] Fatal App Hangs are tracked in RUM. See [#1763][] @@ -635,6 +636,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1763]: https://github.com/DataDog/dd-sdk-ios/pull/1763 [#1767]: https://github.com/DataDog/dd-sdk-ios/pull/1767 [#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 +[#1747]: https://github.com/DataDog/dd-sdk-ios/pull/1747 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu From 746898415884f089654705420ffe53c266565e48 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 2 Apr 2024 13:20:21 +0100 Subject: [PATCH 10/13] RUM-3569 Fix after rebase --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04868667e7..a9bbe3180d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ # 2.9.0 / 11-04-2024 +* [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] * [FEATURE] Call RUM's `errorEventMapper` for crashes. See [#1742][] * [FEATURE] Support calling log event mapper for crashes. See [#1741][] * [FIX] Fix crash in `NetworkInstrumentationFeature`. See [#1767][] From e2b5005a8aaa94f77296222b1a58cc4e5456603f Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 12 Apr 2024 14:27:38 +0100 Subject: [PATCH 11/13] RUM-3569 PR fixes --- .../Sources/Writers/ResourcesWriter.swift | 21 ++++++----- .../Tests/Writer/ResourcesWriterTests.swift | 35 ++++++++++++------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift index b902bb20df..313355101d 100644 --- a/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift +++ b/DatadogSessionReplay/Sources/Writers/ResourcesWriter.swift @@ -16,11 +16,13 @@ internal protocol ResourcesWriting { internal class ResourcesWriter: ResourcesWriting { private let scope: FeatureScope + private let encoder: JSONEncoder + private let decoder: JSONDecoder @ReadWriteLock private var knownIdentifiers = Set() { didSet { - if let knownIdentifiers = knownIdentifiers.asData() { + if let knownIdentifiers = knownIdentifiers.asData(encoder) { scope.dataStore.setValue( knownIdentifiers, forKey: Constants.knownResourcesKey @@ -31,10 +33,13 @@ internal class ResourcesWriter: ResourcesWriting { init( scope: FeatureScope, - dataStoreResetTime: TimeInterval = TimeInterval(30).days + dataStoreResetTime: TimeInterval = TimeInterval(30).days, + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder() ) { self.scope = scope - + self.encoder = encoder + self.decoder = decoder self.scope.dataStore.value(forKey: Constants.storeCreationKey) { [weak self] result in do { if let storeCreation = try result.data()?.asTimeInterval(), Date().timeIntervalSince1970 - storeCreation < dataStoreResetTime { @@ -42,7 +47,7 @@ internal class ResourcesWriter: ResourcesWriting { switch result { case .value(let data, _): do { - if let knownIdentifiers = try data.asKnownIdentifiers() { + if let knownIdentifiers = try data.asKnownIdentifiers(decoder) { self?.knownIdentifiers.formUnion(knownIdentifiers) } } catch let error { @@ -99,8 +104,8 @@ extension Data { return value } - func asKnownIdentifiers() throws -> Set? { - return try JSONDecoder().decode(Set.self, from: self) + func asKnownIdentifiers(_ decoder: JSONDecoder) throws -> Set? { + return try decoder.decode(Set.self, from: self) } } @@ -113,8 +118,8 @@ extension TimeInterval { } extension Set { - func asData() -> Data? { - return try? JSONEncoder().encode(self) // Never fails + func asData(_ encoder: JSONEncoder) -> Data? { + return try? encoder.encode(self) // Never fails } } #endif diff --git a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift index bcc4a5ca25..490f69ec1e 100644 --- a/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift +++ b/DatadogSessionReplay/Tests/Writer/ResourcesWriterTests.swift @@ -25,8 +25,8 @@ class ResourcesWriterTests: XCTestCase { } func testWhenInitialized_itSetsUpDataStore() { - XCTAssertNotNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey]) - XCTAssertNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + XCTAssertNotNil(scopeMock.dataStoreMock.value(forKey: ResourcesWriter.Constants.storeCreationKey)) + XCTAssertNil(scopeMock.dataStoreMock.value(forKey: ResourcesWriter.Constants.knownResourcesKey)) XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) } @@ -43,7 +43,7 @@ class ResourcesWriterTests: XCTestCase { func test_whenWritesSameResourcesToCore_itRemovesDuplicates() throws { // Given - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() + scopeMock.dataStoreMock.setValue(Date().timeIntervalSince1970.asData(), forKey: ResourcesWriter.Constants.storeCreationKey) // When writer.write(resources: [.mockWith(identifier: "1")]) @@ -52,14 +52,15 @@ class ResourcesWriterTests: XCTestCase { // Then XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 1) XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) - let data = try XCTUnwrap(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + let data = try XCTUnwrap(scopeMock.dataStoreMock.value(forKey: ResourcesWriter.Constants.knownResourcesKey)?.data()) XCTAssertGreaterThan(data.count, 0) } func test_whenReadsKnownDuplicates_itDoesNotWriteRecordsToScope() throws { // Given - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["1"]).asData() - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = Date().timeIntervalSince1970.asData() + let knownIdentifiersData = Set(["1"]).asData(JSONEncoder())! + scopeMock.dataStoreMock.setValue(knownIdentifiersData, forKey: ResourcesWriter.Constants.knownResourcesKey) + scopeMock.dataStoreMock.setValue(Date().timeIntervalSince1970.asData(), forKey: ResourcesWriter.Constants.storeCreationKey) let writer = ResourcesWriter(scope: scopeMock) // When @@ -72,23 +73,32 @@ class ResourcesWriterTests: XCTestCase { func test_whenDataStoreIsOlderThan30Days_itClearsKnownDuplicates() throws { // Given - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = Set(["2", "1"]).asData() - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = (Date().timeIntervalSince1970 - 31.days).asData() + let knownIdentifiersData = Set(["2", "1"]).asData(JSONEncoder())! + scopeMock.dataStoreMock.setValue(knownIdentifiersData, forKey: ResourcesWriter.Constants.knownResourcesKey) + scopeMock.dataStoreMock.setValue( + (Date().timeIntervalSince1970 - 31.days).asData(), + forKey: ResourcesWriter.Constants.storeCreationKey + ) + let writer = ResourcesWriter(scope: scopeMock) // When - XCTAssertNil(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey]) + XCTAssertNil(scopeMock.dataStoreMock.value(forKey: ResourcesWriter.Constants.knownResourcesKey)) writer.write(resources: [.mockWith(identifier: "1")]) // Then XCTAssertEqual(scopeMock.eventsWritten(ofType: EnrichedResource.self).count, 1) - XCTAssertEqual(scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey], Set(["1"]).asData()) + XCTAssertEqual( + scopeMock.dataStoreMock.value(forKey: ResourcesWriter.Constants.knownResourcesKey)?.data(), + Set(["1"]).asData(JSONEncoder()) + ) XCTAssertTrue(scopeMock.telemetryMock.messages.isEmpty) } func test_whenKnownResourcesAreBroken_itLogsTelemetry() { // Given - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.knownResourcesKey] = "broken".data(using: .utf8) + let brokenData = "broken".data(using: .utf8)! + scopeMock.dataStoreMock.setValue(brokenData, forKey: ResourcesWriter.Constants.knownResourcesKey) // When _ = ResourcesWriter(scope: scopeMock) @@ -99,7 +109,8 @@ class ResourcesWriterTests: XCTestCase { func test_whenDataStoreCreationIsBroken_itLogsTelemetry() { // Given - scopeMock.dataStoreMock.values[ResourcesWriter.Constants.storeCreationKey] = "broken".data(using: .utf8) + let brokenData = "broken".data(using: .utf8)! + scopeMock.dataStoreMock.setValue(brokenData, forKey: ResourcesWriter.Constants.storeCreationKey) // When _ = ResourcesWriter(scope: scopeMock) From c920d8b57b8b2dea8a1950c9b4d73422cc0f95cf Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 12 Apr 2024 15:52:19 +0100 Subject: [PATCH 12/13] RUM-3569 Fix CHANGELOG --- CHANGELOG.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bbe3180d..3ee935e739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,17 @@ # Unreleased -- [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] -* [FEATURE] Add support for 128 bit trace IDs. See [#1721][] -* [FEATURE] Fatal App Hangs are tracked in RUM. See [#1763][] -* [FIX] Avoid name collision with Required Reason APIs. See [#1774][] +- [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] +- [FEATURE] Add support for 128 bit trace IDs. See [#1721][] +- [FEATURE] Fatal App Hangs are tracked in RUM. See [#1763][] +- [FIX] Avoid name collision with Required Reason APIs. See [#1774][] # 2.9.0 / 11-04-2024 -* [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] -* [FEATURE] Call RUM's `errorEventMapper` for crashes. See [#1742][] -* [FEATURE] Support calling log event mapper for crashes. See [#1741][] -* [FIX] Fix crash in `NetworkInstrumentationFeature`. See [#1767][] -* [FIX] Remove modulemap. See [#1746][] -* [FIX] Expose objc interfaces in Session Replay module. See [#1697][] +- [FEATURE] Call RUM's `errorEventMapper` for crashes. See [#1742][] +- [FEATURE] Support calling log event mapper for crashes. See [#1741][] +- [FIX] Fix crash in `NetworkInstrumentationFeature`. See [#1767][] +- [FIX] Remove modulemap. See [#1746][] +- [FIX] Expose objc interfaces in Session Replay module. See [#1697][] # 2.8.1 / 20-03-2024 From 85647cb8cf15dd7cbdcd83ca93f1befd25321411 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Fri, 12 Apr 2024 15:53:35 +0100 Subject: [PATCH 13/13] RUM-3569 Revert after rebase --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 8ba4f4bc88..f9ad1abc2d 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -552,8 +552,6 @@ 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9EE5AD8226205B82001E699E /* DDNSURLSessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */; }; - A703694D2BB2E30100C66C36 /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703694C2BB2E30100C66C36 /* DataStoreMock.swift */; }; - A703694E2BB2E30100C66C36 /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703694C2BB2E30100C66C36 /* DataStoreMock.swift */; }; A70A82652A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A70ADCD22B583B1300321BC9 /* UIImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70ADCD12B583B1300321BC9 /* UIImageResource.swift */; }; @@ -2521,7 +2519,6 @@ 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReader.swift; sourceTree = ""; }; 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReaderTests.swift; sourceTree = ""; }; 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNSURLSessionDelegateTests.swift; sourceTree = ""; }; - A703694C2BB2E30100C66C36 /* DataStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreMock.swift; sourceTree = ""; }; A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskCoordinator.swift; sourceTree = ""; }; A70ADCD12B583B1300321BC9 /* UIImageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageResource.swift; sourceTree = ""; }; A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriterTests.swift; sourceTree = ""; }; @@ -5662,7 +5659,6 @@ 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */, 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */, - A703694C2BB2E30100C66C36 /* DataStoreMock.swift */, ); path = Mocks; sourceTree = ""; @@ -8403,7 +8399,6 @@ buildActionMask = 2147483647; files = ( 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, - A703694D2BB2E30100C66C36 /* DataStoreMock.swift in Sources */, D2160CF229C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, D257955B298ABB04008A1BE5 /* XCTestCase.swift in Sources */, D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */, @@ -8449,7 +8444,6 @@ buildActionMask = 2147483647; files = ( 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, - A703694E2BB2E30100C66C36 /* DataStoreMock.swift in Sources */, D2160CF329C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, D2579578298ABB83008A1BE5 /* XCTestCase.swift in Sources */, D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */,