From 95fe9ec73cb342673ef25e70ff0b3901e14aafbc Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 27 Nov 2020 16:51:47 +0100 Subject: [PATCH 1/6] RUMM-776 Introduce `addTiming(name:)` API to RUMMonitor --- Sources/Datadog/DDRUMMonitor.swift | 8 ++ .../RUM/RUMEvent/RUMEventBuilder.swift | 9 +- .../RUM/RUMEvent/RUMEventEncoder.swift | 6 ++ .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 9 ++ .../RUMMonitor/Scopes/RUMSessionScope.swift | 2 + .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 26 ++++- Sources/Datadog/RUMMonitor.swift | 12 +++ .../Datadog/Mocks/CoreMocks.swift | 23 +++++ .../Datadog/Mocks/LoggingFeatureMocks.swift | 7 +- .../Datadog/Mocks/RUMFeatureMocks.swift | 21 +++- .../Datadog/Mocks/TracingFeatureMocks.swift | 6 +- .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 99 +++++++++++++++++++ .../Datadog/RUMMonitorTests.swift | 36 ++++++- 13 files changed, 254 insertions(+), 10 deletions(-) diff --git a/Sources/Datadog/DDRUMMonitor.swift b/Sources/Datadog/DDRUMMonitor.swift index 2bfce9244c..13017336b6 100644 --- a/Sources/Datadog/DDRUMMonitor.swift +++ b/Sources/Datadog/DDRUMMonitor.swift @@ -33,6 +33,14 @@ public class DDRUMMonitor { attributes: [AttributeKey: AttributeValue] = [:] ) {} + /// Adds a specific timing in the currently presented View. The timing duration will be computed as the + /// number of nanoseconds between the time the View was started and the time the timing was added. + /// - Parameters: + /// - name: the name of the custom timing attribute. + public func addTiming( + name: String + ) {} + /// Notifies that an Error occurred in currently presented View. /// - Parameters: /// - message: a message explaining the Error. diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift index 6efa7c1ecf..451710404c 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift @@ -13,11 +13,16 @@ internal class RUMEventBuilder { self.userInfoProvider = userInfoProvider } - func createRUMEvent(with model: DM, attributes: [String: Encodable]) -> RUMEvent { + func createRUMEvent( + with model: DM, + attributes: [String: Encodable], + customTimings: [RUMViewCustomTiming]? = nil + ) -> RUMEvent { return RUMEvent( model: model, attributes: attributes, - userInfoAttributes: userInfoProvider.value.extraInfo + userInfoAttributes: userInfoProvider.value.extraInfo, + customViewTimings: customTimings ) } } diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift index 8cd060cb20..8810846fcb 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift @@ -15,6 +15,9 @@ internal struct RUMEvent: Encodable { let attributes: [String: Encodable] let userInfoAttributes: [String: Encodable] + /// Custom View timings (only available if `DM` is a RUM View model) + let customViewTimings: [RUMViewCustomTiming]? + func encode(to encoder: Encoder) throws { try RUMEventEncoder().encode(self, to: encoder) } @@ -40,6 +43,9 @@ internal struct RUMEventEncoder { try event.userInfoAttributes.forEach { attributeName, attributeValue in try attributesContainer.encode(EncodableValue(attributeValue), forKey: DynamicCodingKey("context.usr.\(attributeName)")) } + try event.customViewTimings?.forEach { customTiming in + try attributesContainer.encode(customTiming.duration, forKey: DynamicCodingKey("view.custom_timings.\(customTiming.name)")) + } // Encode `RUMDataModel` try event.model.encode(to: encoder) diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index 06f7fb0bba..1cba57b1c6 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -92,6 +92,15 @@ internal struct RUMAddCurrentViewErrorCommand: RUMCommand { } } +internal struct RUMAddViewTimingCommand: RUMCommand { + let time: Date + var attributes: [AttributeKey: AttributeValue] + + /// The name of the timing. It will be used as a JSON key, whereas the value will be the timing duration, + /// measured since the start of the View. + let timingName: String +} + // MARK: - RUM Resource related commands internal protocol RUMResourceCommand: RUMCommand { diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index dffade88a7..df8579d973 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -74,6 +74,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { identity: expiredViewIdentity, uri: expiredView.viewURI, attributes: expiredView.attributes, + customTimings: expiredView.customTimings, startTime: startTime ) } @@ -123,6 +124,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { identity: command.identity, uri: command.path, attributes: command.attributes, + customTimings: [], startTime: command.time ) ) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 2881284572..020f17a485 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -6,6 +6,13 @@ import Foundation +internal struct RUMViewCustomTiming: Encodable { + /// Timing name. + let name: String + /// Timing duration (in nanoseconds). + let duration: Int64 +} + internal class RUMViewScope: RUMScope, RUMContextProvider { // MARK: - Child Scopes @@ -23,6 +30,8 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private(set) weak var identity: AnyObject? /// View attributes. private(set) var attributes: [AttributeKey: AttributeValue] + /// View custom timings. + private(set) var customTimings: [RUMViewCustomTiming] = [] /// This View's UUID. let viewUUID: RUMUUID @@ -54,12 +63,14 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { identity: AnyObject, uri: String, attributes: [AttributeKey: AttributeValue], + customTimings: [RUMViewCustomTiming], startTime: Date ) { self.parent = parent self.dependencies = dependencies self.identity = identity self.attributes = attributes + self.customTimings = customTimings self.viewUUID = dependencies.rumUUIDGenerator.generateUnique() self.viewURI = uri self.viewStartTime = startTime @@ -113,6 +124,10 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { isActiveView = false needsViewUpdate = true + case let command as RUMAddViewTimingCommand where isActiveView: + addCustomTiming(on: command) + needsViewUpdate = true + // Resource commands case let command as RUMStartResourceCommand where isActiveView: startResource(on: command) @@ -207,6 +222,15 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { ) } + private func addCustomTiming(on command: RUMAddViewTimingCommand) { + customTimings.append( + RUMViewCustomTiming( + name: command.timingName, + duration: command.time.timeIntervalSince(viewStartTime).toInt64Nanoseconds + ) + ) + } + // MARK: - Sending RUM Events private func sendApplicationStartAction(on command: RUMCommand) { @@ -271,7 +295,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dd: .init(documentVersion: version.toInt64) ) - let event = dependencies.eventBuilder.createRUMEvent(with: eventData, attributes: attributes) + let event = dependencies.eventBuilder.createRUMEvent(with: eventData, attributes: attributes, customTimings: customTimings) dependencies.eventOutput.write(rumEvent: event) } diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index 629105ad4d..b936a15e58 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -239,6 +239,18 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { ) } + override public func addTiming( + name: String + ) { + process( + command: RUMAddViewTimingCommand( + time: dateProvider.currentDate(), + attributes: [:], + timingName: name + ) + ) + } + override public func addError( message: String, source: RUMErrorSource, diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index 4cfa6f26b0..30739208ee 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -302,6 +302,29 @@ extension FeaturesCommonDependencies { launchTimeProvider: launchTimeProvider ) } + + /// Creates new instance of `FeaturesCommonDependencies` by replacing individual dependencies. + func replacing( + performance: PerformancePreset? = nil, + httpClient: HTTPClient? = nil, + mobileDevice: MobileDevice? = nil, + dateProvider: DateProvider? = nil, + userInfoProvider: UserInfoProvider? = nil, + networkConnectionInfoProvider: NetworkConnectionInfoProviderType? = nil, + carrierInfoProvider: CarrierInfoProviderType? = nil, + launchTimeProvider: LaunchTimeProviderType? = nil + ) -> FeaturesCommonDependencies { + return FeaturesCommonDependencies( + performance: performance ?? self.performance, + httpClient: httpClient ?? self.httpClient, + mobileDevice: mobileDevice ?? self.mobileDevice, + dateProvider: dateProvider ?? self.dateProvider, + userInfoProvider: userInfoProvider ?? self.userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider ?? self.networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider ?? self.carrierInfoProvider, + launchTimeProvider: launchTimeProvider ?? self.launchTimeProvider + ) + } } class NoOpFileWriter: FileWriterType { diff --git a/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift index 7d18410947..70180d194a 100644 --- a/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift @@ -35,7 +35,12 @@ extension LoggingFeature { dependencies: FeaturesCommonDependencies = .mockAny() ) -> LoggingFeature { // Get the full feature mock: - let fullFeature: LoggingFeature = .mockWith(directory: directory, dependencies: dependencies) + let fullFeature: LoggingFeature = .mockWith( + directory: directory, + dependencies: dependencies.replacing( + dateProvider: SystemDateProvider() // replace date provider in mocked `Feature.Storage` + ) + ) let uploadWorker = DataUploadWorkerMock() let observedStorage = uploadWorker.observe(featureStorage: fullFeature.storage) // Replace by mocking the `FeatureUpload` and observing the `FatureStorage`: diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 19decf79fe..117aaa3fe4 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -36,7 +36,12 @@ extension RUMFeature { dependencies: FeaturesCommonDependencies = .mockAny() ) -> RUMFeature { // Get the full feature mock: - let fullFeature: RUMFeature = .mockWith(directory: directory, dependencies: dependencies) + let fullFeature: RUMFeature = .mockWith( + directory: directory, + dependencies: dependencies.replacing( + dateProvider: SystemDateProvider() // replace date provider in mocked `Feature.Storage` + ) + ) let uploadWorker = DataUploadWorkerMock() let observedStorage = uploadWorker.observe(featureStorage: fullFeature.storage) // Replace by mocking the `FeatureUpload` and observing the `FatureStorage`: @@ -165,6 +170,18 @@ extension RUMAddCurrentViewErrorCommand { } } +extension RUMAddViewTimingCommand { + static func mockWith( + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + timingName: String = .mockAny() + ) -> RUMAddViewTimingCommand { + return RUMAddViewTimingCommand( + time: time, attributes: attributes, timingName: timingName + ) + } +} + extension RUMStartResourceCommand { static func mockAny() -> RUMStartResourceCommand { mockWith() } @@ -417,6 +434,7 @@ extension RUMViewScope { identity: AnyObject = mockView, uri: String = .mockAny(), attributes: [AttributeKey: AttributeValue] = [:], + customTimings: [RUMViewCustomTiming] = [], startTime: Date = .mockAny() ) -> RUMViewScope { return RUMViewScope( @@ -425,6 +443,7 @@ extension RUMViewScope { identity: identity, uri: uri, attributes: attributes, + customTimings: customTimings, startTime: startTime ) } diff --git a/Tests/DatadogTests/Datadog/Mocks/TracingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/TracingFeatureMocks.swift index 953f4331af..df027d5830 100644 --- a/Tests/DatadogTests/Datadog/Mocks/TracingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/TracingFeatureMocks.swift @@ -48,7 +48,11 @@ extension TracingFeature { ) -> TracingFeature { // Get the full feature mock: let fullFeature: TracingFeature = .mockWith( - directory: directory, dependencies: dependencies, loggingFeature: loggingFeature, tracingUUIDGenerator: tracingUUIDGenerator + directory: directory, + dependencies: dependencies.replacing( + dateProvider: SystemDateProvider() // replace date provider in mocked `Feature.Storage` + ), + loggingFeature: loggingFeature, tracingUUIDGenerator: tracingUUIDGenerator ) let uploadWorker = DataUploadWorkerMock() let observedStorage = uploadWorker.observe(featureStorage: fullFeature.storage) diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index 8ffa353aad..cf3a3dc4ec 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -8,6 +8,8 @@ import XCTest import UIKit @testable import Datadog +extension RUMViewCustomTiming: EquatableInTests {} + class RUMViewScopeTests: XCTestCase { private let output = RUMEventOutputMock() private let parent = RUMContextProviderMock() @@ -22,6 +24,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: .mockAny() ) @@ -41,6 +44,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: .mockAny() ) @@ -64,6 +68,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -93,6 +98,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -125,6 +131,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: ["foo": "bar", "fizz": "buzz"], + customTimings: [], startTime: currentTime ) @@ -157,6 +164,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -204,6 +212,7 @@ class RUMViewScopeTests: XCTestCase { identity: view1, uri: "FirstViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -232,6 +241,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "FirstViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -260,6 +270,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: uri, attributes: [:], + customTimings: [], startTime: .mockAny() ) } @@ -294,6 +305,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: Date() ) XCTAssertTrue( @@ -341,6 +353,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: Date() ) @@ -385,6 +398,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: Date() ) XCTAssertTrue( @@ -424,6 +438,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: currentTime ) XCTAssertTrue( @@ -464,6 +479,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: currentTime ) @@ -509,6 +525,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], + customTimings: [], startTime: Date() ) @@ -534,4 +551,86 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(viewUpdate.model.view.resource.count, 0, "Failed Resource should not be counted") XCTAssertEqual(viewUpdate.model.view.error.count, 1, "Failed Resource should be counted as Error") } + + // MARK: - Custom Timings Tracking + + func testGivenActiveView_whenCustomTimingIsRegistered_itSendsViewUpdateEvent() throws { + var currentTime: Date = .mockDecember15th2019At10AMUTC() + let scope = RUMViewScope( + parent: parent, + dependencies: dependencies, + identity: mockView, + uri: "UIViewController", + attributes: [:], + customTimings: [], + startTime: currentTime + ) + XCTAssertTrue( + scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) + ) + + // Given + XCTAssertTrue(scope.isActiveView) + XCTAssertEqual(scope.customTimings.count, 0) + + // When + currentTime.addTimeInterval(0.5) + XCTAssertTrue( + scope.process( + command: RUMAddViewTimingCommand.mockWith(time: currentTime, timingName: "timing-after-500000000ns") + ) + ) + XCTAssertEqual(scope.customTimings.count, 1) + + currentTime.addTimeInterval(0.5) + XCTAssertTrue( + scope.process( + command: RUMAddViewTimingCommand.mockWith(time: currentTime, timingName: "timing-after-1000000000ns") + ) + ) + XCTAssertEqual(scope.customTimings.count, 2) + + // Then + let events = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self)) + let expectedTiming1 = RUMViewCustomTiming(name: "timing-after-500000000ns", duration: 500_000_000) + let expectedTiming2 = RUMViewCustomTiming(name: "timing-after-1000000000ns", duration: 1_000_000_000) + + XCTAssertEqual(events.count, 3, "There should be 3 View updates sent") + XCTAssertEqual(events[0].customViewTimings, []) + XCTAssertEqual(events[1].customViewTimings, [expectedTiming1]) + XCTAssertEqual(events[2].customViewTimings, [expectedTiming1, expectedTiming2]) + } + + func testGivenInactiveView_whenCustomTimingIsRegistered_itDoesNotSendViewUpdateEvent() throws { + var currentTime: Date = .mockDecember15th2019At10AMUTC() + let scope = RUMViewScope( + parent: parent, + dependencies: dependencies, + identity: mockView, + uri: "UIViewController", + attributes: [:], + customTimings: [], + startTime: currentTime + ) + XCTAssertTrue( + scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) + ) + XCTAssertFalse( + scope.process(command: RUMStopViewCommand.mockWith(identity: mockView)) + ) + + // Given + XCTAssertFalse(scope.isActiveView) + + // When + currentTime.addTimeInterval(0.5) + + _ = scope.process( + command: RUMAddViewTimingCommand.mockWith(time: currentTime, timingName: "timing-after-500000000ns") + ) + + // Then + let lastEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) + XCTAssertEqual(lastEvent.customViewTimings, []) + } } diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index dd3343d672..4d2fcfafd7 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -474,10 +474,10 @@ class RUMMonitorTests: XCTestCase { let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 11) let expectedUserInfo = RUMDataUSR(id: "abc-123", name: "Foo", email: "foo@bar.com") - rumEventMatchers.forEach { event in - event.jsonMatcher.assertValue(forKey: "context.usr.str", equals: "value") - event.jsonMatcher.assertValue(forKey: "context.usr.int", equals: 11_235) - event.jsonMatcher.assertValue(forKey: "context.usr.bool", equals: true) + try rumEventMatchers.forEach { event in + XCTAssertEqual(try event.attribute(forKeyPath: "context.usr.str"), "value") + XCTAssertEqual(try event.attribute(forKeyPath: "context.usr.int"), 11_235) + XCTAssertEqual(try event.attribute(forKeyPath: "context.usr.bool"), true) // swiftlint:disable:this xct_specific_matcher } try rumEventMatchers.forEachRUMEvent(ofType: RUMDataAction.self) { action in XCTAssertEqual(action.usr, expectedUserInfo) @@ -614,6 +614,34 @@ class RUMMonitorTests: XCTestCase { try XCTAssertEqual(lastViewUpdate.attribute(forKeyPath: "context.a3"), "foo3", "The attribute should be added") } + // MARK: - Sending Custom Timings + + func testStartingView_thenAddingTiming() throws { + RUMFeature.instance = .mockByRecordingRUMEventMatchers( + directory: temporaryDirectory, + dependencies: .mockWith( + dateProvider: RelativeDateProvider( + startingFrom: Date(), + advancingBySeconds: 1 + ) + ) + ) + defer { RUMFeature.instance = nil } + + let monitor = RUMMonitor.initialize() + setGlobalAttributes(of: monitor) + + monitor.startView(viewController: mockView) + monitor.addTiming(name: "timing1") + monitor.addTiming(name: "timing2") + + let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 4) + verifyGlobalAttributes(in: rumEventMatchers) + let lastViewUpdate = try rumEventMatchers.lastRUMEvent(ofType: RUMDataView.self) + XCTAssertEqual(try lastViewUpdate.attribute(forKeyPath: "view.custom_timings.timing1"), 1_000_000_000) + XCTAssertEqual(try lastViewUpdate.attribute(forKeyPath: "view.custom_timings.timing2"), 2_000_000_000) + } + // MARK: - Thread safety func testRandomlyCallingDifferentAPIsConcurrentlyDoesNotCrash() { From bf7b451be8af5602e7e5d7a59d87b12690dcfe91 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 30 Nov 2020 14:36:28 +0100 Subject: [PATCH 2/6] RUMM-776 Enforce sorting payload keys in alphabetic order this is a workaround for backend issue with overwriting flattened JSON keys when `view.custom_timings` precedes the `view` object in the RUM event payload. --- Sources/Datadog/Core/Utils/JSONEncoder.swift | 4 +++- .../Datadog/Core/Utils/JSONEncoderTests.swift | 22 +++++++++++++++++++ Tests/DatadogTests/Datadog/TracerTests.swift | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Sources/Datadog/Core/Utils/JSONEncoder.swift b/Sources/Datadog/Core/Utils/JSONEncoder.swift index f6a2ae407b..fb7e7e186e 100644 --- a/Sources/Datadog/Core/Utils/JSONEncoder.swift +++ b/Sources/Datadog/Core/Utils/JSONEncoder.swift @@ -15,7 +15,9 @@ extension JSONEncoder { try container.encode(formatted) } if #available(iOS 13.0, OSX 10.15, *) { - encoder.outputFormatting = [.withoutEscapingSlashes] + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + } else { + encoder.outputFormatting = [.sortedKeys] } return encoder } diff --git a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift index 5e4fe43f8f..6277601ca1 100644 --- a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift @@ -29,4 +29,26 @@ class JSONEncoderTests: XCTestCase { XCTAssertEqual(encodedURL.utf8String, #"{"value":"https:\/\/example.com\/foo"}"#) } } + + func testWhenEncoding_thenKeysFollowLexicographicOrder() throws { + struct Foo: Codable { + var one = 1 + var two = 1 + var three = 1 + var four = 1 + + enum CodingKeys: String, CodingKey { + case one = "aaaaaa" + case two = "bb" + case three = "aaa" + case four = "bbb" + } + } + + // When + let encodedFoo = try jsonEncoder.encode(Foo()) + + // Then + XCTAssertEqual(encodedFoo.utf8String, #"{"aaa":1,"aaaaaa":1,"bb":1,"bbb":1}"#) + } } diff --git a/Tests/DatadogTests/Datadog/TracerTests.swift b/Tests/DatadogTests/Datadog/TracerTests.swift index db9e504c4c..a9186bd11e 100644 --- a/Tests/DatadogTests/Datadog/TracerTests.swift +++ b/Tests/DatadogTests/Datadog/TracerTests.swift @@ -582,7 +582,7 @@ class TracerTests: XCTestCase { ) XCTAssertEqual( try spanMatcher.meta.custom(keyPath: "meta.person"), - #"{"name":"Adam","age":30,"nationality":"Polish"}"# + #"{"age":30,"name":"Adam","nationality":"Polish"}"# ) XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.nested.string"), "hello") XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.url"), "https://example.com/image.png") From ea42d325e3da3e6b618febcc2b59b1001240af36 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 30 Nov 2020 15:00:32 +0100 Subject: [PATCH 3/6] RUMM-776 Add integration tests --- .../SendRUMFixture1ViewController.swift | 6 ++++++ .../RUM/RUMManualInstrumentationScenarioTests.swift | 9 ++++++++- Tests/DatadogTests/Datadog/RUMMonitorTests.swift | 4 ++-- Tests/DatadogTests/Matchers/RUMEventMatcher.swift | 4 ++++ Tests/DatadogTests/Matchers/RUMSessionMatcher.swift | 12 ++++++++---- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Datadog/Example/Scenarios/RUM/ManualInstrumentation/SendRUMFixture1ViewController.swift b/Datadog/Example/Scenarios/RUM/ManualInstrumentation/SendRUMFixture1ViewController.swift index dde82a9599..f6dcf17375 100644 --- a/Datadog/Example/Scenarios/RUM/ManualInstrumentation/SendRUMFixture1ViewController.swift +++ b/Datadog/Example/Scenarios/RUM/ManualInstrumentation/SendRUMFixture1ViewController.swift @@ -20,6 +20,10 @@ internal class SendRUMFixture1ViewController: UIViewController { super.viewDidAppear(animated) rumMonitor.startView(viewController: self) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + rumMonitor.addTiming(name: "content-ready") + } } override func viewWillDisappear(_ animated: Bool) { @@ -29,6 +33,8 @@ internal class SendRUMFixture1ViewController: UIViewController { } @IBAction func didTapDownloadResourceButton(_ sender: Any) { + rumMonitor.addTiming(name: "first-interaction") + let simulatedResourceKey1 = "/resource/1" let simulatedResourceRequest1 = URLRequest(url: URL(string: "https://foo.com/resource/1")!) let simulatedResourceKey2 = "/resource/2" diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift index e7196a91b8..8d1cd99760 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMManualInstrumentationScenarioTests.swift @@ -54,7 +54,7 @@ class RUMManualInstrumentationScenarioTests: IntegrationTests, RUMCommonAsserts let view1 = session.viewVisits[0] XCTAssertEqual(view1.path, "SendRUMFixture1ViewController") - XCTAssertEqual(view1.viewEvents.count, 4, "First view should receive 4 updates") + XCTAssertEqual(view1.viewEvents.count, 6, "First view should receive 6 updates") XCTAssertEqual(view1.viewEvents.last?.view.action.count, 2) XCTAssertEqual(view1.viewEvents.last?.view.resource.count, 1) XCTAssertEqual(view1.viewEvents.last?.view.error.count, 1) @@ -77,6 +77,13 @@ class RUMManualInstrumentationScenarioTests: IntegrationTests, RUMCommonAsserts XCTAssertEqual(view1.errorEvents[0].error.resource?.method, .methodGET) XCTAssertEqual(view1.errorEvents[0].error.resource?.statusCode, 400) + let contentReadyTiming = try XCTUnwrap(view1.viewEventMatchers.last?.timing(named: "content-ready")) + let firstInteractionTiming = try XCTUnwrap(view1.viewEventMatchers.last?.timing(named: "first-interaction")) + XCTAssertGreaterThanOrEqual(contentReadyTiming, 50_000) + XCTAssertLessThan(contentReadyTiming, 1_000_000_000) + XCTAssertGreaterThan(firstInteractionTiming, 0) + XCTAssertLessThan(firstInteractionTiming, 5_000_000_000) + let view2 = session.viewVisits[1] XCTAssertEqual(view2.path, "SendRUMFixture2ViewController") XCTAssertEqual(view2.viewEvents.last?.view.action.count, 0) diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index 4d2fcfafd7..be84989a00 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -638,8 +638,8 @@ class RUMMonitorTests: XCTestCase { let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 4) verifyGlobalAttributes(in: rumEventMatchers) let lastViewUpdate = try rumEventMatchers.lastRUMEvent(ofType: RUMDataView.self) - XCTAssertEqual(try lastViewUpdate.attribute(forKeyPath: "view.custom_timings.timing1"), 1_000_000_000) - XCTAssertEqual(try lastViewUpdate.attribute(forKeyPath: "view.custom_timings.timing2"), 2_000_000_000) + XCTAssertEqual(try lastViewUpdate.timing(named: "timing1"), 1_000_000_000) + XCTAssertEqual(try lastViewUpdate.timing(named: "timing2"), 2_000_000_000) } // MARK: - Thread safety diff --git a/Tests/DatadogTests/Matchers/RUMEventMatcher.swift b/Tests/DatadogTests/Matchers/RUMEventMatcher.swift index 793838a550..d691d0876b 100644 --- a/Tests/DatadogTests/Matchers/RUMEventMatcher.swift +++ b/Tests/DatadogTests/Matchers/RUMEventMatcher.swift @@ -95,6 +95,10 @@ internal class RUMEventMatcher { func attribute(forKeyPath keyPath: String) throws -> T { return try jsonMatcher.value(forKeyPath: keyPath) } + + func timing(named timingName: String) throws -> Int64 { + return try attribute(forKeyPath: "view.custom_timings.\(timingName)") + } } extension RUMEventMatcher: CustomStringConvertible { diff --git a/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift b/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift index 547261d65f..e24a686202 100644 --- a/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift +++ b/Tests/DatadogTests/Matchers/RUMSessionMatcher.swift @@ -53,6 +53,9 @@ internal class RUMSessionMatcher { /// `RUMView` events tracked during this visit. fileprivate(set) var viewEvents: [RUMDataView] = [] + /// `RUMEventMatchers` corresponding to item in `viewEvents`. + fileprivate(set) var viewEventMatchers: [RUMEventMatcher] = [] + /// `RUMAction` events tracked during this visit. fileprivate(set) var actionEvents: [RUMDataAction] = [] @@ -81,8 +84,8 @@ internal class RUMSessionMatcher { // Get RUM Events by kind: - let viewEvents: [RUMDataView] = try (eventsMatchersByType["view"] ?? []) - .map { matcher in try matcher.model() } + let viewEventMatchers = eventsMatchersByType["view"] ?? [] + let viewEvents: [RUMDataView] = try viewEventMatchers.map { matcher in try matcher.model() } let actionEvents: [RUMDataAction] = try (eventsMatchersByType["action"] ?? []) .map { matcher in try matcher.model() } @@ -103,10 +106,11 @@ internal class RUMSessionMatcher { var visitsByViewID: [String: ViewVisit] = [:] visits.forEach { visit in visitsByViewID[visit.viewID] = visit } - // Group RUM Events by View Visits: - try viewEvents.forEach { rumEvent in + // Group RUM Events and their matchers by View Visits: + try zip(viewEvents, viewEventMatchers).forEach { rumEvent, matcher in if let visit = visitsByViewID[rumEvent.view.id] { visit.viewEvents.append(rumEvent) + visit.viewEventMatchers.append(matcher) if visit.path.isEmpty { visit.path = rumEvent.view.url } else if visit.path != rumEvent.view.url { From 369a2b45f2175825ddea24252c7c0b53e41e5a03 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 30 Nov 2020 15:10:52 +0100 Subject: [PATCH 4/6] RUMM-776 Represent view timings as plain dictionary --- .../RUM/RUMEvent/RUMEventBuilder.swift | 2 +- .../RUM/RUMEvent/RUMEventEncoder.swift | 6 +-- .../RUMMonitor/Scopes/RUMSessionScope.swift | 2 +- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 24 ++------- .../Datadog/Mocks/RUMFeatureMocks.swift | 2 +- .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 52 ++++++++++--------- 6 files changed, 37 insertions(+), 51 deletions(-) diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift index 451710404c..eefbd566d6 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift @@ -16,7 +16,7 @@ internal class RUMEventBuilder { func createRUMEvent( with model: DM, attributes: [String: Encodable], - customTimings: [RUMViewCustomTiming]? = nil + customTimings: [String: Int64]? = nil ) -> RUMEvent { return RUMEvent( model: model, diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift index 8810846fcb..085a1a7acf 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift @@ -16,7 +16,7 @@ internal struct RUMEvent: Encodable { let userInfoAttributes: [String: Encodable] /// Custom View timings (only available if `DM` is a RUM View model) - let customViewTimings: [RUMViewCustomTiming]? + let customViewTimings: [String: Int64]? func encode(to encoder: Encoder) throws { try RUMEventEncoder().encode(self, to: encoder) @@ -43,8 +43,8 @@ internal struct RUMEventEncoder { try event.userInfoAttributes.forEach { attributeName, attributeValue in try attributesContainer.encode(EncodableValue(attributeValue), forKey: DynamicCodingKey("context.usr.\(attributeName)")) } - try event.customViewTimings?.forEach { customTiming in - try attributesContainer.encode(customTiming.duration, forKey: DynamicCodingKey("view.custom_timings.\(customTiming.name)")) + try event.customViewTimings?.forEach { timingName, timingDuration in + try attributesContainer.encode(timingDuration, forKey: DynamicCodingKey("view.custom_timings.\(timingName)")) } // Encode `RUMDataModel` diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index df8579d973..145f560266 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -124,7 +124,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { identity: command.identity, uri: command.path, attributes: command.attributes, - customTimings: [], + customTimings: [:], startTime: command.time ) ) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 020f17a485..6fe67c1262 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -6,13 +6,6 @@ import Foundation -internal struct RUMViewCustomTiming: Encodable { - /// Timing name. - let name: String - /// Timing duration (in nanoseconds). - let duration: Int64 -} - internal class RUMViewScope: RUMScope, RUMContextProvider { // MARK: - Child Scopes @@ -30,8 +23,8 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private(set) weak var identity: AnyObject? /// View attributes. private(set) var attributes: [AttributeKey: AttributeValue] - /// View custom timings. - private(set) var customTimings: [RUMViewCustomTiming] = [] + /// View custom timings, keyed by name. The value of timing is given in nanoseconds. + private(set) var customTimings: [String: Int64] = [:] /// This View's UUID. let viewUUID: RUMUUID @@ -63,7 +56,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { identity: AnyObject, uri: String, attributes: [AttributeKey: AttributeValue], - customTimings: [RUMViewCustomTiming], + customTimings: [String: Int64], startTime: Date ) { self.parent = parent @@ -125,7 +118,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { needsViewUpdate = true case let command as RUMAddViewTimingCommand where isActiveView: - addCustomTiming(on: command) + customTimings[command.timingName] = command.time.timeIntervalSince(viewStartTime).toInt64Nanoseconds needsViewUpdate = true // Resource commands @@ -222,15 +215,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { ) } - private func addCustomTiming(on command: RUMAddViewTimingCommand) { - customTimings.append( - RUMViewCustomTiming( - name: command.timingName, - duration: command.time.timeIntervalSince(viewStartTime).toInt64Nanoseconds - ) - ) - } - // MARK: - Sending RUM Events private func sendApplicationStartAction(on command: RUMCommand) { diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 117aaa3fe4..6d0486dd32 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -434,7 +434,7 @@ extension RUMViewScope { identity: AnyObject = mockView, uri: String = .mockAny(), attributes: [AttributeKey: AttributeValue] = [:], - customTimings: [RUMViewCustomTiming] = [], + customTimings: [String: Int64] = [:], startTime: Date = .mockAny() ) -> RUMViewScope { return RUMViewScope( diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index cf3a3dc4ec..82f4fdd207 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -8,8 +8,6 @@ import XCTest import UIKit @testable import Datadog -extension RUMViewCustomTiming: EquatableInTests {} - class RUMViewScopeTests: XCTestCase { private let output = RUMEventOutputMock() private let parent = RUMContextProviderMock() @@ -24,7 +22,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: .mockAny() ) @@ -44,7 +42,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: .mockAny() ) @@ -68,7 +66,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -98,7 +96,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -131,7 +129,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: ["foo": "bar", "fizz": "buzz"], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -164,7 +162,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -212,7 +210,7 @@ class RUMViewScopeTests: XCTestCase { identity: view1, uri: "FirstViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -241,7 +239,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "FirstViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -270,7 +268,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: uri, attributes: [:], - customTimings: [], + customTimings: [:], startTime: .mockAny() ) } @@ -305,7 +303,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: Date() ) XCTAssertTrue( @@ -353,7 +351,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: Date() ) @@ -398,7 +396,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: Date() ) XCTAssertTrue( @@ -438,7 +436,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) XCTAssertTrue( @@ -479,7 +477,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) @@ -525,7 +523,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: Date() ) @@ -562,7 +560,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) XCTAssertTrue( @@ -592,13 +590,17 @@ class RUMViewScopeTests: XCTestCase { // Then let events = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self)) - let expectedTiming1 = RUMViewCustomTiming(name: "timing-after-500000000ns", duration: 500_000_000) - let expectedTiming2 = RUMViewCustomTiming(name: "timing-after-1000000000ns", duration: 1_000_000_000) XCTAssertEqual(events.count, 3, "There should be 3 View updates sent") - XCTAssertEqual(events[0].customViewTimings, []) - XCTAssertEqual(events[1].customViewTimings, [expectedTiming1]) - XCTAssertEqual(events[2].customViewTimings, [expectedTiming1, expectedTiming2]) + XCTAssertEqual(events[0].customViewTimings, [:]) + XCTAssertEqual( + events[1].customViewTimings, + ["timing-after-500000000ns": 500_000_000] + ) + XCTAssertEqual( + events[2].customViewTimings, + ["timing-after-500000000ns": 500_000_000, "timing-after-1000000000ns": 1_000_000_000] + ) } func testGivenInactiveView_whenCustomTimingIsRegistered_itDoesNotSendViewUpdateEvent() throws { @@ -609,7 +611,7 @@ class RUMViewScopeTests: XCTestCase { identity: mockView, uri: "UIViewController", attributes: [:], - customTimings: [], + customTimings: [:], startTime: currentTime ) XCTAssertTrue( @@ -631,6 +633,6 @@ class RUMViewScopeTests: XCTestCase { // Then let lastEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) - XCTAssertEqual(lastEvent.customViewTimings, []) + XCTAssertEqual(lastEvent.customViewTimings, [:]) } } From 0aac69992cd3f16480e4fabb9ced6cd98816de72 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 30 Nov 2020 15:26:48 +0100 Subject: [PATCH 5/6] RUMM-776 Fix benchmark tests compilation issue --- .../DataStorage/RUMStorageBenchmarkTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift index 23b3ab98b8..379f389ce9 100644 --- a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift @@ -106,7 +106,8 @@ class RUMStorageBenchmarkTests: XCTestCase { dd: .init(documentVersion: .mockAny()) ), attributes: ["attribute": "value"], - userInfoAttributes: ["str": "value", "int": 11_235, "bool": true] + userInfoAttributes: ["str": "value", "int": 11_235, "bool": true], + customViewTimings: nil ) } } From 69091871fa0700525cb58e6058ad65ac6c665e79 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 1 Dec 2020 12:48:45 +0100 Subject: [PATCH 6/6] RUMM-776 CR improvements --- Sources/Datadog/Core/Utils/JSONEncoder.swift | 16 ++++++++++++++++ Sources/Datadog/DDRUMMonitor.swift | 2 +- .../Datadog/Core/Utils/JSONEncoderTests.swift | 7 ++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Sources/Datadog/Core/Utils/JSONEncoder.swift b/Sources/Datadog/Core/Utils/JSONEncoder.swift index fb7e7e186e..ebb45e79bb 100644 --- a/Sources/Datadog/Core/Utils/JSONEncoder.swift +++ b/Sources/Datadog/Core/Utils/JSONEncoder.swift @@ -15,6 +15,22 @@ extension JSONEncoder { try container.encode(formatted) } if #available(iOS 13.0, OSX 10.15, *) { + // NOTE: The `.sortedKeys` option was added in RUMM-776 after discovering an issue + // with backend processing of the RUM View payloads. The custom timings encoding for + // RUM views requires following structure: + // + // ``` + // { + // view: { /* serialized, auto-generated RUM view event */ }, + // view.custom_timings.: , + // view.custom_timings.: + // ... + // } + // ``` + // + // To guarantee proper backend-side processing, the `view.custom_timings` keys must be + // encoded after the `view` object. Using `.sortedKeys` enforces this order. + // encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] } else { encoder.outputFormatting = [.sortedKeys] diff --git a/Sources/Datadog/DDRUMMonitor.swift b/Sources/Datadog/DDRUMMonitor.swift index 13017336b6..86ee5694ad 100644 --- a/Sources/Datadog/DDRUMMonitor.swift +++ b/Sources/Datadog/DDRUMMonitor.swift @@ -36,7 +36,7 @@ public class DDRUMMonitor { /// Adds a specific timing in the currently presented View. The timing duration will be computed as the /// number of nanoseconds between the time the View was started and the time the timing was added. /// - Parameters: - /// - name: the name of the custom timing attribute. + /// - name: the name of the custom timing attribute. It must be unique for each timing. public func addTiming( name: String ) {} diff --git a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift index 6277601ca1..dfa74a9df4 100644 --- a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift @@ -36,12 +36,14 @@ class JSONEncoderTests: XCTestCase { var two = 1 var three = 1 var four = 1 + var five = 1 enum CodingKeys: String, CodingKey { case one = "aaaaaa" case two = "bb" case three = "aaa" case four = "bbb" + case five = "aaa.aaa" } } @@ -49,6 +51,9 @@ class JSONEncoderTests: XCTestCase { let encodedFoo = try jsonEncoder.encode(Foo()) // Then - XCTAssertEqual(encodedFoo.utf8String, #"{"aaa":1,"aaaaaa":1,"bb":1,"bbb":1}"#) + XCTAssertEqual( + encodedFoo.utf8String, + #"{"aaa":1,"aaa.aaa":1,"aaaaaa":1,"bb":1,"bbb":1}"# + ) } }