diff --git a/Datadog/Example/Debugging/DebugRUMViewController.swift b/Datadog/Example/Debugging/DebugRUMViewController.swift index 895c96f8ac..063f5db4f7 100644 --- a/Datadog/Example/Debugging/DebugRUMViewController.swift +++ b/Datadog/Example/Debugging/DebugRUMViewController.swift @@ -156,8 +156,12 @@ class DebugRUMViewController: UIViewController { /// Creates an instance of `UIViewController` subclass with a given name. private func createUIViewControllerSubclassInstance(named viewControllerClassName: String) -> UIViewController { - let theClass: AnyClass = objc_allocateClassPair(UIViewController.classForCoder(), viewControllerClassName, 0)! - objc_registerClassPair(theClass) + let theClass: AnyClass = NSClassFromString(viewControllerClassName) ?? { + let cls: AnyClass + cls = objc_allocateClassPair(UIViewController.classForCoder(), viewControllerClassName, 0)! + objc_registerClassPair(cls) + return cls + }() return theClass.alloc() as! UIViewController } diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift index 1cc4d4c852..db54a0571b 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -33,7 +33,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { ) // Set user information - Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com") + Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com", extraInfo: ["key-extraUserInfo": "value-extraUserInfo"]) // Create Logger logger = Logger.builder diff --git a/Sources/Datadog/Core/Attributes/UserInfoProvider.swift b/Sources/Datadog/Core/Attributes/UserInfoProvider.swift index 02a84e0c57..585c17a957 100644 --- a/Sources/Datadog/Core/Attributes/UserInfoProvider.swift +++ b/Sources/Datadog/Core/Attributes/UserInfoProvider.swift @@ -12,11 +12,11 @@ internal class UserInfoProvider { /// `UserInfo` can be mutated by any user thread with `Datadog.setUserInfo(id:name:email:)` - at the same /// time it might be accessed by different queues running in the SDK. private let queue = DispatchQueue(label: "com.datadoghq.user-info-provider", qos: .userInteractive) - private var current = UserInfo(id: nil, name: nil, email: nil) + private var _value = UserInfo(id: nil, name: nil, email: nil, extraInfo: [:]) var value: UserInfo { - set { queue.async { self.current = newValue } } - get { queue.sync { self.current } } + set { queue.async { self._value = newValue } } + get { queue.sync { self._value } } } } @@ -25,4 +25,5 @@ internal struct UserInfo { let id: String? let name: String? let email: String? + let extraInfo: [AttributeKey: AttributeValue] } diff --git a/Sources/Datadog/Datadog.swift b/Sources/Datadog/Datadog.swift index 0bf15a1193..7b9e55d070 100644 --- a/Sources/Datadog/Datadog.swift +++ b/Sources/Datadog/Datadog.swift @@ -75,12 +75,25 @@ public class Datadog { } } + /// Sets current user information. + /// Those will be added to logs, traces and RUM events automatically. + /// - Parameters: + /// - id: User ID, if any + /// - name: Name representing the user, if any + /// - email: User's email, if any + /// - extraInfo: User's custom attributes, if any public static func setUserInfo( id: String? = nil, name: String? = nil, - email: String? = nil + email: String? = nil, + extraInfo: [AttributeKey: AttributeValue] = [:] ) { - instance?.userInfoProvider.value = UserInfo(id: id, name: name, email: email) + instance?.userInfoProvider.value = UserInfo( + id: id, + name: name, + email: email, + extraInfo: extraInfo + ) } // MARK: - Internal diff --git a/Sources/Datadog/Logging/Log/LogEncoder.swift b/Sources/Datadog/Logging/Log/LogEncoder.swift index a9bff89d51..ee19712ada 100644 --- a/Sources/Datadog/Logging/Log/LogEncoder.swift +++ b/Sources/Datadog/Logging/Log/LogEncoder.swift @@ -137,13 +137,19 @@ internal struct LogEncoder { // Encode attributes... var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self) - // ... first, user attributes ... + // 1. user info attributes + try log.userInfo.extraInfo.forEach { + let key = DynamicCodingKey("usr.\($0)") + try attributesContainer.encode(EncodableValue($1), forKey: key) + } + + // 2. user attributes let encodableUserAttributes = Dictionary( uniqueKeysWithValues: log.attributes.userAttributes.map { name, value in (name, EncodableValue(value)) } ) try encodableUserAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } - // ... then, internal attributes: + // 3. internal attributes if let internalAttributes = log.attributes.internalAttributes { let encodableInternalAttributes = Dictionary( uniqueKeysWithValues: internalAttributes.map { name, value in (name, EncodableValue(value)) } diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift index e2ba7c2e6f..6efa7c1ecf 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventBuilder.swift @@ -6,8 +6,18 @@ import Foundation -internal struct RUMEventBuilder { +internal class RUMEventBuilder { + let userInfoProvider: UserInfoProvider + + init(userInfoProvider: UserInfoProvider) { + self.userInfoProvider = userInfoProvider + } + func createRUMEvent(with model: DM, attributes: [String: Encodable]) -> RUMEvent { - return RUMEvent(model: model, attributes: attributes) + return RUMEvent( + model: model, + attributes: attributes, + userInfoAttributes: userInfoProvider.value.extraInfo + ) } } diff --git a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift index 6b8a1da908..8cd060cb20 100644 --- a/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift +++ b/Sources/Datadog/RUM/RUMEvent/RUMEventEncoder.swift @@ -13,6 +13,7 @@ internal struct RUMEvent: Encodable { /// Custom attributes set by the user let attributes: [String: Encodable] + let userInfoAttributes: [String: Encodable] func encode(to encoder: Encoder) throws { try RUMEventEncoder().encode(self, to: encoder) @@ -36,6 +37,9 @@ internal struct RUMEventEncoder { try event.attributes.forEach { attributeName, attributeValue in try attributesContainer.encode(EncodableValue(attributeValue), forKey: DynamicCodingKey("context.\(attributeName)")) } + try event.userInfoAttributes.forEach { attributeName, attributeValue in + try attributesContainer.encode(EncodableValue(attributeValue), forKey: DynamicCodingKey("context.usr.\(attributeName)")) + } // Encode `RUMDataModel` try event.model.encode(to: encoder) diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index be2a150930..629105ad4d 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -182,7 +182,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { networkConnectionInfoProvider: rumFeature.networkConnectionInfoProvider, carrierInfoProvider: rumFeature.carrierInfoProvider ), - eventBuilder: RUMEventBuilder(), + eventBuilder: RUMEventBuilder(userInfoProvider: rumFeature.userInfoProvider), eventOutput: RUMEventFileOutput( fileWriter: rumFeature.storage.writer ), diff --git a/Sources/Datadog/Tracing/Span/SpanBuilder.swift b/Sources/Datadog/Tracing/Span/SpanBuilder.swift index 36d4f9a1c0..3f6b7a6b01 100644 --- a/Sources/Datadog/Tracing/Span/SpanBuilder.swift +++ b/Sources/Datadog/Tracing/Span/SpanBuilder.swift @@ -27,14 +27,18 @@ internal struct SpanBuilder { func createSpan(from ddspan: DDSpan, finishTime: Date) -> Span { let tagsReducer = SpanTagsReducer(spanTags: ddspan.tags, logFields: ddspan.logFields) - var jsonStringEncodedTags: [String: JSONStringEncodableValue] = [:] - - // First, add baggage items as tags... + var jsonStringEncodedTags = [String: JSONStringEncodableValue]() + // 1. add user info attributes as tags + for (itemKey, itemValue) in userInfoProvider.value.extraInfo { + let encodedKey = "usr.\(itemKey)" + let encodableValue = JSONStringEncodableValue(itemValue, encodedUsing: tagsJSONEncoder) + jsonStringEncodedTags[encodedKey] = encodableValue + } + // 2. add baggage items as tags for (itemKey, itemValue) in ddspan.ddContext.baggageItems.all { jsonStringEncodedTags[itemKey] = JSONStringEncodableValue(itemValue, encodedUsing: tagsJSONEncoder) } - - // ... then, add regular tags + // 3. add regular tags for (tagName, tagValue) in tagsReducer.reducedSpanTags { jsonStringEncodedTags[tagName] = JSONStringEncodableValue(tagValue, encodedUsing: tagsJSONEncoder) } diff --git a/Tests/DatadogBenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift index 424229322a..ea42edf6e6 100644 --- a/Tests/DatadogBenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataStorage/LoggingStorageBenchmarkTests.swift @@ -80,7 +80,7 @@ class LoggingStorageBenchmarkTests: XCTestCase { loggerVersion: "0.0.0", threadName: "main", applicationVersion: "0.0.0", - userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com"), + userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com", extraInfo: ["str": "value", "int": 11_235, "bool": true]), networkConnectionInfo: .init( reachability: .yes, availableInterfaces: [.cellular], diff --git a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift index 6d67df540c..23b3ab98b8 100644 --- a/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataStorage/RUMStorageBenchmarkTests.swift @@ -105,7 +105,8 @@ class RUMStorageBenchmarkTests: XCTestCase { connectivity: nil, dd: .init(documentVersion: .mockAny()) ), - attributes: ["attribute": "value"] + attributes: ["attribute": "value"], + userInfoAttributes: ["str": "value", "int": 11_235, "bool": true] ) } } diff --git a/Tests/DatadogBenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift index 7be3dbb199..257dd1dd74 100644 --- a/Tests/DatadogBenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataStorage/TracingStorageBenchmarkTests.swift @@ -92,7 +92,7 @@ class TracingStorageBenchmarkTests: XCTestCase { isConstrained: false ), mobileCarrierInfo: nil, - userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com"), + userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com", extraInfo: ["str": "value", "int": 11_235, "bool": true]), tags: [ "tag": JSONStringEncodableValue("value", encodedUsing: JSONEncoder()) ] diff --git a/Tests/DatadogTests/Datadog/LoggerTests.swift b/Tests/DatadogTests/Datadog/LoggerTests.swift index 8d73022465..8d246dfc3f 100644 --- a/Tests/DatadogTests/Datadog/LoggerTests.swift +++ b/Tests/DatadogTests/Datadog/LoggerTests.swift @@ -186,16 +186,37 @@ class LoggerTests: XCTestCase { Datadog.setUserInfo(id: "abc-123", name: "Foo") logger.debug("message with user `id` and `name`") - Datadog.setUserInfo(id: "abc-123", name: "Foo", email: "foo@example.com") - logger.debug("message with user `id`, `name` and `email`") + Datadog.setUserInfo( + id: "abc-123", + name: "Foo", + email: "foo@example.com", + extraInfo: [ + "str": "value", + "int": 11_235, + "bool": true + ] + ) + logger.debug("message with user `id`, `name`, `email` and `extraInfo`") Datadog.setUserInfo(id: nil, name: nil, email: nil) logger.debug("message with no user info") let logMatchers = try LoggingFeature.waitAndReturnLogMatchers(count: 4) logMatchers[0].assertUserInfo(equals: nil) + logMatchers[1].assertUserInfo(equals: (id: "abc-123", name: "Foo", email: nil)) - logMatchers[2].assertUserInfo(equals: (id: "abc-123", name: "Foo", email: "foo@example.com")) + + logMatchers[2].assertUserInfo( + equals: ( + id: "abc-123", + name: "Foo", + email: "foo@example.com" + ) + ) + logMatchers[2].assertValue(forKey: "usr.str", equals: "value") + logMatchers[2].assertValue(forKey: "usr.int", equals: 11_235) + logMatchers[2].assertValue(forKey: "usr.bool", equals: true) + logMatchers[3].assertUserInfo(equals: nil) } diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index 0e0f8d7c7b..4cfa6f26b0 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -382,7 +382,7 @@ extension UserInfo { } static func mockEmpty() -> UserInfo { - return UserInfo(id: nil, name: nil, email: nil) + return UserInfo(id: nil, name: nil, email: nil, extraInfo: [:]) } } diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 50c3bd7587..19decf79fe 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -80,7 +80,7 @@ struct RUMDataModelMock: RUMDataModel, Equatable { extension RUMEventBuilder { static func mockAny() -> RUMEventBuilder { - return RUMEventBuilder() + return RUMEventBuilder(userInfoProvider: UserInfoProvider.mockAny()) } } @@ -323,7 +323,7 @@ extension RUMScopeDependencies { networkConnectionInfoProvider: NetworkConnectionInfoProviderMock(networkConnectionInfo: nil), carrierInfoProvider: CarrierInfoProviderMock(carrierInfo: nil) ), - eventBuilder: RUMEventBuilder = RUMEventBuilder(), + eventBuilder: RUMEventBuilder = RUMEventBuilder(userInfoProvider: UserInfoProvider.mockAny()), eventOutput: RUMEventOutput = RUMEventOutputMock(), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator() ) -> RUMScopeDependencies { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventBuilderTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventBuilderTests.swift index 2de99c5443..37fb6d3883 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventBuilderTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMEventBuilderTests.swift @@ -9,7 +9,7 @@ import XCTest class RUMEventBuilderTests: XCTestCase { func testItBuildsRUMEvent() { - let builder = RUMEventBuilder() + let builder = RUMEventBuilder(userInfoProvider: UserInfoProvider.mockAny()) let event = builder.createRUMEvent( with: RUMDataModelMock(attribute: "foo"), attributes: ["foo": "bar", "fizz": "buzz"] diff --git a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMUserInfoProviderTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMUserInfoProviderTests.swift index f0979e8365..76aa1fc1b2 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMUserInfoProviderTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMEvent/RUMUserInfoProviderTests.swift @@ -19,13 +19,16 @@ class RUMUserInfoProviderTests: XCTestCase { } func testWhenUserInfoIsAvailable_itReturnsRUMUserInfo() { - userInfoProvider.value = UserInfo(id: "abc-123", name: nil, email: nil) + userInfoProvider.value = UserInfo(id: "abc-123", name: nil, email: nil, extraInfo: [:]) XCTAssertEqual(rumUserInfoProvider.current, RUMDataUSR(id: "abc-123", name: nil, email: nil)) - userInfoProvider.value = UserInfo(id: "abc-123", name: "Foo", email: nil) + userInfoProvider.value = UserInfo(id: "abc-123", name: "Foo", email: nil, extraInfo: [:]) XCTAssertEqual(rumUserInfoProvider.current, RUMDataUSR(id: "abc-123", name: "Foo", email: nil)) - userInfoProvider.value = UserInfo(id: "abc-123", name: "Foo", email: "foo@bar.com") + userInfoProvider.value = UserInfo(id: "abc-123", name: "Foo", email: "foo@bar.com", extraInfo: [:]) + XCTAssertEqual(rumUserInfoProvider.current, RUMDataUSR(id: "abc-123", name: "Foo", email: "foo@bar.com")) + + userInfoProvider.value = UserInfo(id: "abc-123", name: "Foo", email: "foo@bar.com", extraInfo: [:]) XCTAssertEqual(rumUserInfoProvider.current, RUMDataUSR(id: "abc-123", name: "Foo", email: "foo@bar.com")) } } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMEventOutputs/RUMEventFileOutputTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMEventOutputs/RUMEventFileOutputTests.swift index 91c506a0b7..dce443e5e7 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMEventOutputs/RUMEventFileOutputTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMEventOutputs/RUMEventFileOutputTests.swift @@ -21,7 +21,7 @@ class RUMEventFileOutputTests: XCTestCase { func testItWritesRUMEventToFileAsJSON() throws { let fileCreationDateProvider = RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC()) let queue = DispatchQueue(label: "com.datadohq.testItWritesRUMEventToFileAsJSON") - let builder = RUMEventBuilder() + let builder = RUMEventBuilder(userInfoProvider: UserInfoProvider.mockAny()) let output = RUMEventFileOutput( fileWriter: FileWriter( dataFormat: RUMFeature.dataFormat, diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index b815d7c2a4..dd3343d672 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -445,7 +445,16 @@ class RUMMonitorTests: XCTestCase { directory: temporaryDirectory, dependencies: .mockWith( userInfoProvider: .mockWith( - userInfo: UserInfo(id: "abc-123", name: "Foo", email: "foo@bar.com") + userInfo: UserInfo( + id: "abc-123", + name: "Foo", + email: "foo@bar.com", + extraInfo: [ + "str": "value", + "int": 11_235, + "bool": true + ] + ) ) ) ) @@ -465,6 +474,11 @@ 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.forEachRUMEvent(ofType: RUMDataAction.self) { action in XCTAssertEqual(action.usr, expectedUserInfo) } diff --git a/Tests/DatadogTests/Datadog/TracerTests.swift b/Tests/DatadogTests/Datadog/TracerTests.swift index 8c1825b421..db9e504c4c 100644 --- a/Tests/DatadogTests/Datadog/TracerTests.swift +++ b/Tests/DatadogTests/Datadog/TracerTests.swift @@ -335,8 +335,17 @@ class TracerTests: XCTestCase { Datadog.setUserInfo(id: "abc-123", name: "Foo") tracer.startSpan(operationName: "span with user `id` and `name`").finish() - Datadog.setUserInfo(id: "abc-123", name: "Foo", email: "foo@example.com") - tracer.startSpan(operationName: "span with user `id`, `name` and `email`").finish() + Datadog.setUserInfo( + id: "abc-123", + name: "Foo", + email: "foo@example.com", + extraInfo: [ + "str": "value", + "int": 11_235, + "bool": true + ] + ) + tracer.startSpan(operationName: "span with user `id`, `name`, `email` and `extraInfo`").finish() Datadog.setUserInfo(id: nil, name: nil, email: nil) tracer.startSpan(operationName: "span with no user info").finish() @@ -353,6 +362,9 @@ class TracerTests: XCTestCase { XCTAssertEqual(try spanMatchers[2].meta.userID(), "abc-123") XCTAssertEqual(try spanMatchers[2].meta.userName(), "Foo") XCTAssertEqual(try spanMatchers[2].meta.userEmail(), "foo@example.com") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.str"), "value") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.int"), "11235") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.bool"), "true") XCTAssertNil(try? spanMatchers[3].meta.userID()) XCTAssertNil(try? spanMatchers[3].meta.userName())