diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 38ddfb970e..bb26e5cb60 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -486,6 +486,7 @@ 9E307C3224C8846D0039607E /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; 9E359F4E26CD518D001E25E9 /* LongTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */; }; 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; + 9E53889C2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */; }; 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */; }; 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */; }; 9E55407C25812D1C00F6E3AD /* RUMMonitor+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E55407B25812D1C00F6E3AD /* RUMMonitor+objc.swift */; }; @@ -502,6 +503,7 @@ 9E9973F1268DF69500D8059B /* VitalInfoSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */; }; 9EA3CA6926775A3500B16871 /* VitalRefreshRateReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */; }; 9EA8A7F1275E1518007D6FDB /* HostsSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA8A7F0275E1518007D6FDB /* HostsSanitizerTests.swift */; }; + 9EA8A7F82768A72B007D6FDB /* WebLogEventConsumerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */; }; 9EAF0CF6275A21100044E8CA /* WKUserContentController+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */; }; 9EAF0CF8275A2FDC0044E8CA /* HostsSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAF0CF7275A2FDC0044E8CA /* HostsSanitizer.swift */; }; 9EB4B862274E79D50041CD03 /* WKUserContentController+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB4B861274E79D50041CD03 /* WKUserContentController+Datadog.swift */; }; @@ -1150,6 +1152,7 @@ 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoSamplerTests.swift; sourceTree = ""; }; 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongTaskObserver.swift; sourceTree = ""; }; 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionsTests.swift; sourceTree = ""; }; + 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRUMEventConsumerTests.swift; sourceTree = ""; }; 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzler.swift; sourceTree = ""; }; 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzlerTests.swift; sourceTree = ""; }; 9E55407B25812D1C00F6E3AD /* RUMMonitor+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMMonitor+objc.swift"; sourceTree = ""; }; @@ -1167,6 +1170,7 @@ 9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = ""; }; 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReader.swift; sourceTree = ""; }; 9EA8A7F0275E1518007D6FDB /* HostsSanitizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsSanitizerTests.swift; sourceTree = ""; }; + 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLogEventConsumerTests.swift; sourceTree = ""; }; 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+DatadogTests.swift"; sourceTree = ""; }; 9EAF0CF7275A2FDC0044E8CA /* HostsSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsSanitizer.swift; sourceTree = ""; }; 9EB4B861274E79D50041CD03 /* WKUserContentController+Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKUserContentController+Datadog.swift"; sourceTree = ""; }; @@ -3295,6 +3299,8 @@ children = ( 9EB4B86B27510AF90041CD03 /* WebEventBridgeTests.swift */, 9EAF0CF5275A21100044E8CA /* WKUserContentController+DatadogTests.swift */, + 9EA8A7F72768A72B007D6FDB /* WebLogEventConsumerTests.swift */, + 9E53889B2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift */, ); path = WebView; sourceTree = ""; @@ -4114,6 +4120,7 @@ 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */, 61B038BA2527257B00518F3C /* URLSessionAutoInstrumentationMocks.swift in Sources */, 61133C672423990D00786299 /* LogConsoleOutputTests.swift in Sources */, + 9E53889C2773C4B300A7DC42 /* WebRUMEventConsumerTests.swift in Sources */, 6114FDEC257659E90084E372 /* FeatureDirectoriesMock.swift in Sources */, 61122EE825B1C92500F9C7F5 /* SpanSanitizerTests.swift in Sources */, 61B5E42526DFAFBC000B0A5F /* DDGlobal+apiTests.m in Sources */, @@ -4216,6 +4223,7 @@ 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */, 61F8CC092469295500FE2908 /* DatadogConfigurationBuilderTests.swift in Sources */, 61F1A623249B811200075390 /* Encoding.swift in Sources */, + 9EA8A7F82768A72B007D6FDB /* WebLogEventConsumerTests.swift in Sources */, 6114FE3B25768AA90084E372 /* ConsentProviderTests.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift b/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift index e3baf4b917..8d9a188f3a 100644 --- a/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift +++ b/Sources/Datadog/FeaturesIntegration/WebView/WKUserContentController+Datadog.swift @@ -16,10 +16,33 @@ public extension WKUserContentController { internal func __addDatadogMessageHandler(allowedWebViewHosts: Set, hostsSanitizer: HostsSanitizing) { let bridgeName = DatadogMessageHandler.name + let contextProvider = (Global.rum as? RUMMonitor)?.contextProvider + + var logEventConsumer: DefaultWebLogEventConsumer? = nil + if let loggingFeature = LoggingFeature.instance { + logEventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: loggingFeature.storage.writer, + internalLogsWriter: InternalMonitoringFeature.instance?.logsStorage.writer, + dateCorrector: loggingFeature.dateCorrector, + rumContextProvider: contextProvider, + applicationVersion: loggingFeature.configuration.common.applicationVersion, + environment: loggingFeature.configuration.common.environment + ) + } + + var rumEventConsumer: DefaultWebRUMEventConsumer? = nil + if let rumFeature = RUMFeature.instance { + rumEventConsumer = DefaultWebRUMEventConsumer( + dataWriter: rumFeature.storage.writer, + dateCorrector: rumFeature.dateCorrector, + contextProvider: contextProvider + ) + } + let messageHandler = DatadogMessageHandler( eventBridge: WebEventBridge( - logEventConsumer: WebLogEventConsumer(), - rumEventConsumer: WebRUMEventConsumer() + logEventConsumer: logEventConsumer, + rumEventConsumer: rumEventConsumer ) ) add(messageHandler, name: bridgeName) @@ -57,10 +80,10 @@ public extension WKUserContentController { } } -private class DatadogMessageHandler: NSObject, WKScriptMessageHandler { +internal class DatadogMessageHandler: NSObject, WKScriptMessageHandler { static let name = "DatadogEventBridge" private let eventBridge: WebEventBridge - private let queue = DispatchQueue( + let queue = DispatchQueue( label: "com.datadoghq.JSEventBridge", target: .global(qos: .userInteractive) ) @@ -73,9 +96,11 @@ private class DatadogMessageHandler: NSObject, WKScriptMessageHandler { _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { + // message.body must be called within UI thread + let messageBody = message.body queue.async { do { - try self.eventBridge.consume(message.body) + try self.eventBridge.consume(messageBody) } catch { userLogger.error("🔥 Web Event Error: \(error)") } diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift index 457a7f3bdc..6be1dd93a0 100644 --- a/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebEventBridge.swift @@ -8,8 +8,12 @@ import Foundation internal typealias JSON = [String: Any] -internal protocol WebEventConsumer { - func consume(event: JSON, eventType: String) +internal protocol WebLogEventConsumer { + func consume(event: JSON, internalLog: Bool) throws +} + +internal protocol WebRUMEventConsumer { + func consume(event: JSON) throws } internal enum WebEventError: Error, Equatable { @@ -24,12 +28,13 @@ internal class WebEventBridge { static let eventTypeKey = "eventType" static let eventKey = "event" static let eventTypeLog = "log" + static let eventTypeInternalLog = "internal_log" } - private let logEventConsumer: WebEventConsumer - private let rumEventConsumer: WebEventConsumer + private let logEventConsumer: WebLogEventConsumer? + private let rumEventConsumer: WebRUMEventConsumer? - init(logEventConsumer: WebEventConsumer, rumEventConsumer: WebEventConsumer) { + init(logEventConsumer: WebLogEventConsumer?, rumEventConsumer: WebRUMEventConsumer?) { self.logEventConsumer = logEventConsumer self.rumEventConsumer = rumEventConsumer } @@ -46,10 +51,22 @@ internal class WebEventBridge { throw WebEventError.missingKey(key: Constants.eventKey) } - if eventType == Constants.eventTypeLog { - logEventConsumer.consume(event: wrappedEvent, eventType: eventType) + if eventType == Constants.eventTypeLog || + eventType == Constants.eventTypeInternalLog { + if let consumer = logEventConsumer { + try consumer.consume( + event: wrappedEvent, + internalLog: (eventType == Constants.eventTypeInternalLog) + ) + } else { + userLogger.warn("A WebView log is lost because Logging is disabled in iOS SDK") + } } else { - rumEventConsumer.consume(event: wrappedEvent, eventType: eventType) + if let consumer = rumEventConsumer { + try consumer.consume(event: wrappedEvent) + } else { + userLogger.warn("A WebView RUM event is lost because RUM is disabled in iOS SDK") + } } } diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift index b9935ce23c..53902f06e0 100644 --- a/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebLogEventConsumer.swift @@ -6,8 +6,78 @@ import Foundation -internal class WebLogEventConsumer: WebEventConsumer { - func consume(event: [String: Any], eventType: String) { - // TODO: RUMM-1791 implement event consumers +internal class DefaultWebLogEventConsumer: WebLogEventConsumer { + private struct Constants { + static let logEventType = "log" + static let internalLogEventType = "internal_log" + + static let applicationIDKey = "application_id" + static let sessionIDKey = "session_id" + static let ddTagsKey = "ddtags" + static let dateKey = "date" + } + + private let userLogsWriter: Writer + private let internalLogsWriter: Writer? + private let dateCorrector: DateCorrectorType + private let rumContextProvider: RUMContextProvider? + private let applicationVersion: String + private let environment: String + + private let jsonDecoder = JSONDecoder() + + private lazy var ddTags: String = { + let versionKey = LogEventEncoder.StaticCodingKeys.applicationVersion.rawValue + let versionValue = applicationVersion + let envKey = LogEventEncoder.StaticCodingKeys.environment.rawValue + let envValue = environment + + return "\(versionKey):\(versionValue),\(envKey):\(envValue)" + }() + + init( + userLogsWriter: Writer, + internalLogsWriter: Writer?, + dateCorrector: DateCorrectorType, + rumContextProvider: RUMContextProvider?, + applicationVersion: String, + environment: String + ) { + self.userLogsWriter = userLogsWriter + self.internalLogsWriter = internalLogsWriter + self.dateCorrector = dateCorrector + self.rumContextProvider = rumContextProvider + self.applicationVersion = applicationVersion + self.environment = environment + } + + func consume(event: JSON, internalLog: Bool) throws { + var mutableEvent = event + + if let existingTags = mutableEvent[Constants.ddTagsKey] as? String, !existingTags.isEmpty { + mutableEvent[Constants.ddTagsKey] = "\(ddTags),\(existingTags)" + } else { + mutableEvent[Constants.ddTagsKey] = ddTags + } + + if let date = mutableEvent[Constants.dateKey] as? Int { + let serverTimeOffsetInNs = dateCorrector.currentCorrection.serverTimeOffset.toInt64Nanoseconds + let correctedDate = Int64(date) + serverTimeOffsetInNs + mutableEvent[Constants.dateKey] = correctedDate + } + + if let context = rumContextProvider?.context { + mutableEvent[Constants.applicationIDKey] = context.rumApplicationID + mutableEvent[Constants.sessionIDKey] = context.sessionID.toRUMDataFormat + } + + let jsonData = try JSONSerialization.data(withJSONObject: mutableEvent, options: []) + let encodableEvent = try jsonDecoder.decode(CodableValue.self, from: jsonData) + + if internalLog { + internalLogsWriter?.write(value: encodableEvent) + } else { + userLogsWriter.write(value: encodableEvent) + } } } diff --git a/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift b/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift index 9a4df60d05..cdfcff6b01 100644 --- a/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift +++ b/Sources/Datadog/FeaturesIntegration/WebView/WebRUMEventConsumer.swift @@ -6,8 +6,93 @@ import Foundation -internal class WebRUMEventConsumer: WebEventConsumer { - func consume(event: [String: Any], eventType: String) { - // TODO: RUMM-1791 implement event consumers +internal class DefaultWebRUMEventConsumer: WebRUMEventConsumer { + private let dataWriter: Writer + private let dateCorrector: DateCorrectorType + private let contextProvider: RUMContextProvider? + + private let jsonDecoder = JSONDecoder() + + init( + dataWriter: Writer, + dateCorrector: DateCorrectorType, + contextProvider: RUMContextProvider? + ) { + self.dataWriter = dataWriter + self.dateCorrector = dateCorrector + self.contextProvider = contextProvider + } + + func consume(event: JSON) throws { + let rumContext = contextProvider?.context + let mappedEvent = map(event: event, with: rumContext) + + let jsonData = try JSONSerialization.data(withJSONObject: mappedEvent, options: []) + let encodableEvent = try jsonDecoder.decode(CodableValue.self, from: jsonData) + + dataWriter.write(value: encodableEvent) + } + + private func map(event: JSON, with context: RUMContext?) -> JSON { + guard let context = context, + context.sessionID != .nullUUID else { + return event + } + + var mutableEvent = event + + if let date = mutableEvent["date"] as? Int { + let viewID = (mutableEvent["view"] as? JSON)?["id"] as? String + let serverTimeOffsetInNs = getOffset(viewID: viewID) + let correctedDate = Int64(date) + serverTimeOffsetInNs + mutableEvent["date"] = correctedDate + } + + if let context = contextProvider?.context { + if var application = mutableEvent["application"] as? JSON { + application["id"] = context.rumApplicationID + mutableEvent["application"] = application + } + if var session = mutableEvent["session"] as? JSON { + session["id"] = context.sessionID.toRUMDataFormat + mutableEvent["session"] = session + } + } + + if var dd = mutableEvent["_dd"] as? JSON, + var dd_sesion = dd["session"] as? [String: Int] { + dd_sesion["plan"] = 1 + dd["session"] = dd_sesion + mutableEvent["_dd"] = dd + } + + return mutableEvent + } + + // MARK: - Time offsets + + private typealias Offset = Int64 + private typealias ViewIDOffsetPair = (viewID: String, offset: Offset) + private var viewIDOffsetPairs = [ViewIDOffsetPair]() + + private func getOffset(viewID: String?) -> Offset { + guard let viewID = viewID else { + return 0 + } + + purgeOffsets() + let found = viewIDOffsetPairs.first { $0.viewID == viewID } + if let found = found { + return found.offset + } + let offset = dateCorrector.currentCorrection.serverTimeOffset.toInt64Nanoseconds + viewIDOffsetPairs.insert((viewID: viewID, offset: offset), at: 0) + return offset + } + + private func purgeOffsets() { + while viewIDOffsetPairs.count > 3 { + _ = viewIDOffsetPairs.popLast() + } } } diff --git a/Sources/Datadog/Logging/Log/LogEventEncoder.swift b/Sources/Datadog/Logging/Log/LogEventEncoder.swift index 58663c2808..252c6290d9 100644 --- a/Sources/Datadog/Logging/Log/LogEventEncoder.swift +++ b/Sources/Datadog/Logging/Log/LogEventEncoder.swift @@ -89,6 +89,7 @@ internal struct LogEventEncoder { case status case message case serviceName = "service" + case environment = "env" case tags = "ddtags" // MARK: - Error diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift index 4fad15162f..fcf523e35c 100644 --- a/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WKUserContentController+DatadogTests.swift @@ -9,14 +9,41 @@ import WebKit @testable import Datadog final class DDUserContentController: WKUserContentController { - private(set) var messageHandlerNames = [String]() + typealias NameHandlerPair = (name: String, handler: WKScriptMessageHandler) + private(set) var messageHandlers = [NameHandlerPair]() override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { - messageHandlerNames.append(name) + messageHandlers.append((name: name, handler: scriptMessageHandler)) } } +final class MockScriptMessage: WKScriptMessage { + let mockBody: Any + + init(body: Any) { + self.mockBody = body + } + + override var body: Any { return mockBody } +} + class WKUserContentController_DatadogTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(RUMFeature.instance) + temporaryFeatureDirectories.create() + } + + override func tearDown() { + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(RUMFeature.instance) + temporaryFeatureDirectories.delete() + super.tearDown() + } + func testItAddsUserScriptAndMessageHandler() throws { let mockSanitizer = MockHostsSanitizer() let controller = DDUserContentController() @@ -26,10 +53,97 @@ class WKUserContentController_DatadogTests: XCTestCase { controller.__addDatadogMessageHandler(allowedWebViewHosts: ["datadoghq.com"], hostsSanitizer: mockSanitizer) XCTAssertEqual(controller.userScripts.count, initialUserScriptCount + 1) - XCTAssertEqual(controller.messageHandlerNames, ["DatadogEventBridge"]) + XCTAssertEqual(controller.messageHandlers.map({ $0.name }), ["DatadogEventBridge"]) + XCTAssertEqual(mockSanitizer.sanitizations.count, 1) let sanitization = try XCTUnwrap(mockSanitizer.sanitizations.first) XCTAssertEqual(sanitization.hosts, ["datadoghq.com"]) XCTAssertEqual(sanitization.warningMessage, "The allowed WebView host configured for Datadog SDK is not valid") } + + func testItLogsInvalidWebMessages() throws { + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + let output = LogOutputMock() + userLogger = .mockWith(logOutput: output) + + let controller = DDUserContentController() + controller.__addDatadogMessageHandler(allowedWebViewHosts: ["datadoghq.com"], hostsSanitizer: MockHostsSanitizer()) + + let messageHandler = try XCTUnwrap(controller.messageHandlers.first?.handler) as? DatadogMessageHandler + // non-string body is passed + messageHandler?.userContentController(controller, didReceive: MockScriptMessage(body: 123)) + messageHandler?.queue.sync { } + + XCTAssertEqual(output.recordedLog?.status, .error) + let userLogMessage = try XCTUnwrap(output.recordedLog?.message) + XCTAssertEqual(userLogMessage, #"🔥 Web Event Error: invalidMessage(description: "123")"#) + } + + func testSendingWebEvents() throws { + let dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) + LoggingFeature.instance = .mockByRecordingLogMatchers( + directories: temporaryFeatureDirectories, + configuration: .mockWith( + common: .mockWith( + applicationVersion: "1.0.0", + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + serviceName: "default-service-name", + environment: "tests", + sdkVersion: "1.2.3" + ) + ), + dependencies: .mockWith( + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + ) + ) + RUMFeature.instance = .mockByRecordingRUMEventMatchers( + directories: temporaryFeatureDirectories, + dependencies: .mockWith( + dateProvider: dateProvider + ) + ) + Global.rum = RUMMonitor.initialize() + defer { + LoggingFeature.instance?.deinitialize() + Global.rum = DDNoopRUMMonitor() + RUMFeature.instance?.deinitialize() + } + + let controller = DDUserContentController() + controller.__addDatadogMessageHandler( + allowedWebViewHosts: ["datadoghq.com"], + hostsSanitizer: MockHostsSanitizer() + ) + + let messageHandler = try XCTUnwrap(controller.messageHandlers.first?.handler) as? DatadogMessageHandler + let webLogMessage = MockScriptMessage(body: #"{"eventType":"log","event":{"date":1635932927012,"error":{"origin":"console"},"message":"console error: error","session_id":"0110cab4-7471-480e-aa4e-7ce039ced355","status":"error","view":{"referrer":"","url":"https://datadoghq.dev/browser-sdk-test-playground"}},"tags":["browser_sdk_version:3.6.13"]}"#) + messageHandler?.userContentController(controller, didReceive: webLogMessage) + + let logMatcher = try LoggingFeature.waitAndReturnLogMatchers(count: 1)[0] + + logMatcher.assertValue(forKey: "date", equals: 1_635_932_927_012) + logMatcher.assertValue(forKey: "ddtags", equals: "version:1.0.0,env:tests") + logMatcher.assertValue(forKey: "message", equals: "console error: error") + logMatcher.assertValue(forKey: "status", equals: "error") + logMatcher.assertValue( + forKey: "view", + equals: ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ) + logMatcher.assertValue( + forKey: "error", + equals: ["origin": "console"] + ) + // 00000000-0000-0000-0000-000000000000 is session_id of mock RUM context + logMatcher.assertValue(forKey: "session_id", equals: "00000000-0000-0000-0000-000000000000") + + let webRUMMessage = MockScriptMessage(body: #"{"eventType":"view","event":{"application":{"id":"xxx"},"date":1635933113708,"service":"super","session":{"id":"0110cab4-7471-480e-aa4e-7ce039ced355","type":"user"},"type":"view","view":{"action":{"count":0},"cumulative_layout_shift":0,"dom_complete":152800000,"dom_content_loaded":118300000,"dom_interactive":116400000,"error":{"count":0},"first_contentful_paint":121300000,"id":"64308fd4-83f9-48cb-b3e1-1e91f6721230","in_foreground_periods":[],"is_active":true,"largest_contentful_paint":121299000,"load_event":152800000,"loading_time":152800000,"loading_type":"initial_load","long_task":{"count":0},"referrer":"","resource":{"count":3},"time_spent":3120000000,"url":"http://localhost:8080/test.html"},"_dd":{"document_version":2,"drift":0,"format_version":2,"session":{"plan":2}}},"tags":["browser_sdk_version:3.6.13"]}"#) + messageHandler?.userContentController(controller, didReceive: webRUMMessage) + + let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 1) + try rumEventMatchers[0].model(ofType: RUMViewEvent.self) { rumModel in + XCTAssertEqual(rumModel.application.id, "xxx") + XCTAssertEqual(rumModel.view.id, "64308fd4-83f9-48cb-b3e1-1e91f6721230") + } + } } diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift index 3ffde28db5..757f96db3f 100644 --- a/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebEventBridgeTests.swift @@ -7,11 +7,21 @@ import XCTest @testable import Datadog -fileprivate class MockEventConsumer: WebEventConsumer { - private(set) var consumedEvents: [(event: JSON, eventType: String)] = [] +fileprivate class MockEventConsumer: WebLogEventConsumer, WebRUMEventConsumer { + private(set) var consumedLogEvents: [JSON] = [] + private(set) var consumedInternalLogEvents: [JSON] = [] + private(set) var consumedRUMEvents: [JSON] = [] - func consume(event: JSON, eventType: String) { - consumedEvents.append((event: event, eventType: eventType)) + func consume(event: JSON, internalLog: Bool) throws { + if internalLog { + consumedInternalLogEvents.append(event) + } else { + consumedLogEvents.append(event) + } + } + + func consume(event: JSON) throws { + consumedRUMEvents.append(event) } } @@ -58,13 +68,16 @@ class WebEventBridgeTests: XCTestCase { """ try eventBridge.consume(messageLog) - XCTAssertEqual(mockLogEventConsumer.consumedEvents.count, 1) - XCTAssertEqual(mockRUMEventConsumer.consumedEvents.count, 0) + XCTAssertEqual(mockLogEventConsumer.consumedLogEvents.count, 1) + XCTAssertEqual(mockLogEventConsumer.consumedInternalLogEvents.count, 0) + XCTAssertEqual(mockLogEventConsumer.consumedRUMEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedLogEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedInternalLogEvents.count, 0) + XCTAssertEqual(mockRUMEventConsumer.consumedRUMEvents.count, 0) - let consumedEvent = try XCTUnwrap(mockLogEventConsumer.consumedEvents.first) - XCTAssertEqual(consumedEvent.eventType, "log") - XCTAssertEqual(consumedEvent.event["session_id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") - XCTAssertEqual((consumedEvent.event["view"] as? JSON)?["url"] as? String, "https://datadoghq.dev/browser-sdk-test-playground") + let consumedEvent = try XCTUnwrap(mockLogEventConsumer.consumedLogEvents.first) + XCTAssertEqual(consumedEvent["session_id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") + XCTAssertEqual((consumedEvent["view"] as? JSON)?["url"] as? String, "https://datadoghq.dev/browser-sdk-test-playground") } func testWhenEventTypeIsNonLog_itGoesToRUMEventConsumer() throws { @@ -73,12 +86,14 @@ class WebEventBridgeTests: XCTestCase { """ try eventBridge.consume(messageRUM) - XCTAssertEqual(mockLogEventConsumer.consumedEvents.count, 0) - XCTAssertEqual(mockRUMEventConsumer.consumedEvents.count, 1) + XCTAssertEqual( + mockLogEventConsumer.consumedLogEvents.count + mockLogEventConsumer.consumedInternalLogEvents.count, + 0 + ) + XCTAssertEqual(mockRUMEventConsumer.consumedRUMEvents.count, 1) - let consumedEvent = try XCTUnwrap(mockRUMEventConsumer.consumedEvents.first) - XCTAssertEqual(consumedEvent.eventType, "view") - XCTAssertEqual((consumedEvent.event["session"] as? JSON)?["id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") - XCTAssertEqual((consumedEvent.event["view"] as? JSON)?["url"] as? String, "http://localhost:8080/test.html") + let consumedEvent = try XCTUnwrap(mockRUMEventConsumer.consumedRUMEvents.first) + XCTAssertEqual((consumedEvent["session"] as? JSON)?["id"] as? String, "0110cab4-7471-480e-aa4e-7ce039ced355") + XCTAssertEqual((consumedEvent["view"] as? JSON)?["url"] as? String, "http://localhost:8080/test.html") } } diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift new file mode 100644 index 0000000000..be1c9ea46e --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebLogEventConsumerTests.swift @@ -0,0 +1,136 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class WebLogEventConsumerTests: XCTestCase { + let mockUserLogsWriter = FileWriterMock() + let mockInternalLogsWriter = FileWriterMock() + let mockDateCorrector = DateCorrectorMock() + let mockContextProvider = RUMContextProviderMock(context: .mockWith(rumApplicationID: "123456")) + + func testWhenValidWebLogEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID = UUID(uuidString: "e9796469-c2a1-43d6-b0f6-65c47d33cf5f")! + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: mockContextProvider, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + let expectedWebLogEvent: JSON = [ + "date": 1_635_932_927_012 + 123.toInt64Nanoseconds, + "error": ["origin": "console"], + "message": "console error: error", + "application_id": "123456", + "session_id": mockSessionID.uuidString.lowercased(), + "status": "error", + "ddtags": "version:\(applicationVersion),env:\(environment)", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + + try eventConsumer.consume(event: webLogEvent, internalLog: false) + + let data = try JSONEncoder().encode(mockUserLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockInternalLogsWriter.dataWritten) + } + + func testWhenValidWebInternalLogEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID = UUID(uuidString: "e9796469-c2a1-43d6-b0f6-65c47d33cf5f")! + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: mockContextProvider, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + let expectedWebLogEvent: JSON = [ + "date": 1_635_932_927_012 + 123.toInt64Nanoseconds, + "error": ["origin": "console"], + "message": "console error: error", + "application_id": "123456", + "session_id": mockSessionID.uuidString.lowercased(), + "status": "error", + "ddtags": "version:\(applicationVersion),env:\(environment)", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + + try eventConsumer.consume(event: webLogEvent, internalLog: true) + + let data = try JSONEncoder().encode(mockInternalLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockUserLogsWriter.dataWritten) + } + + func testWhenContextIsUnavailable_itPassesEventAsIs() throws { + let applicationVersion = String.mockRandom() + let environment = String.mockRandom() + let eventConsumer = DefaultWebLogEventConsumer( + userLogsWriter: mockUserLogsWriter, + internalLogsWriter: mockInternalLogsWriter, + dateCorrector: mockDateCorrector, + rumContextProvider: nil, + applicationVersion: applicationVersion, + environment: environment + ) + + let webLogEvent: JSON = [ + "date": 1_635_932_927_012, + "error": ["origin": "console"], + "message": "console error: error", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "error", + "view": ["referrer": "", "url": "https://datadoghq.dev/browser-sdk-test-playground"] + ] + var expectedWebLogEvent: JSON = webLogEvent + expectedWebLogEvent["ddtags"] = "version:\(applicationVersion),env:\(environment)" + + try eventConsumer.consume(event: webLogEvent, internalLog: false) + + let data = try JSONEncoder().encode(mockUserLogsWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebLogEvent) + + XCTAssertNil(mockInternalLogsWriter.dataWritten) + } +} diff --git a/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift b/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift new file mode 100644 index 0000000000..02b16ab9af --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/WebView/WebRUMEventConsumerTests.swift @@ -0,0 +1,126 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +// TODO: RUMM-1786 test mutations (session_id, application_id, date) +class WebRUMEventConsumerTests: XCTestCase { + let mockWriter = FileWriterMock() + let mockDateCorrector = DateCorrectorMock() + let mockContextProvider = RUMContextProviderMock(context: .mockWith(rumApplicationID: "123456")) + + func testWhenValidWebRUMEventPassed_itDecoratesAndPassesToWriter() throws { + let mockSessionID = UUID(uuidString: "e9796469-c2a1-43d6-b0f6-65c47d33cf5f")! + mockContextProvider.context.sessionID = RUMUUID(rawValue: mockSessionID) + mockDateCorrector.correctionOffset = 123 + let eventConsumer = DefaultWebRUMEventConsumer(dataWriter: mockWriter, dateCorrector: mockDateCorrector, contextProvider: mockContextProvider) + + let webRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 2] + ], + "application": ["id": "75d50c62-8b66-403c-a453-aaa1c44d64bd"], + "date": 1_640_252_823_292, + "service": "shopist-web-ui", + "session": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + let expectedWebRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 1] + ], + "application": ["id": mockContextProvider.context.rumApplicationID], + "date": 1_640_252_823_292 + 123.toInt64Nanoseconds, + "service": "shopist-web-ui", + "session": ["id": mockContextProvider.context.sessionID.toRUMDataFormat], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, expectedWebRUMEvent) + } + + func testWhenValidWebRUMEventPassedWithoutRUMContext_itPassesToWriter() throws { + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: nil + ) + + let webRUMEvent: JSON = [ + "_dd": [ + "session": ["plan": 2] + ], + "application": ["id": "75d50c62-8b66-403c-a453-aaa1c44d64bd"], + "date": 1_640_252_823_292, + "service": "shopist-web-ui", + "session": ["id": "00000000-aaaa-0000-aaaa-000000000000"], + "view": [ + "id": "00413060-599f-4a77-80de-5d3beab3da2e" + ], + "type": "action" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, webRUMEvent) + } + + func testWhenNativeSessionIsSampledOut_itPassesWebEventToWriter() throws { + mockContextProvider.context.sessionID = RUMUUID.nullUUID + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: mockContextProvider + ) + + let webRUMEvent: JSON = [ + "new_key": "new_value", + "type": "unknown" + ] + + try eventConsumer.consume(event: webRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, webRUMEvent) + } + + func testWhenUnknownWebRUMEventPassed_itPassesToWriter() throws { + let eventConsumer = DefaultWebRUMEventConsumer( + dataWriter: mockWriter, + dateCorrector: mockDateCorrector, + contextProvider: mockContextProvider + ) + + let unknownWebRUMEvent: JSON = [ + "new_key": "new_value", + "type": "unknown" + ] + + try eventConsumer.consume(event: unknownWebRUMEvent) + + let data = try JSONEncoder().encode(mockWriter.dataWritten as? CodableValue) + let writtenJSON = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? JSON) + + AssertDictionariesEqual(writtenJSON, unknownWebRUMEvent) + } +}