diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 43f34b955f..0d4bc1b759 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -40,7 +40,7 @@ 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB12423979B00786299 /* DataUploadWorker.swift */; }; 61133BD82423979B00786299 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB22423979B00786299 /* HTTPClient.swift */; }; 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB32423979B00786299 /* DataUploadDelay.swift */; }; - 61133BDA2423979B00786299 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB42423979B00786299 /* HTTPHeaders.swift */; }; + 61133BDA2423979B00786299 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB42423979B00786299 /* RequestBuilder.swift */; }; 61133BDB2423979B00786299 /* DatadogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB52423979B00786299 /* DatadogConfiguration.swift */; }; 61133BDC2423979B00786299 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB62423979B00786299 /* Logger.swift */; }; 61133BDD2423979B00786299 /* InternalLoggers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB82423979B00786299 /* InternalLoggers.swift */; }; @@ -79,7 +79,7 @@ 61133C5D2423990D00786299 /* DataUploadConditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C302423990D00786299 /* DataUploadConditionsTests.swift */; }; 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C312423990D00786299 /* DataUploadDelayTests.swift */; }; 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C322423990D00786299 /* DataUploaderTests.swift */; }; - 61133C602423990D00786299 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C332423990D00786299 /* HTTPHeadersTests.swift */; }; + 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C332423990D00786299 /* RequestBuilderTests.swift */; }; 61133C612423990D00786299 /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C342423990D00786299 /* HTTPClientTests.swift */; }; 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C362423990D00786299 /* InternalLoggersTests.swift */; }; 61133C642423990D00786299 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C382423990D00786299 /* LoggerTests.swift */; }; @@ -382,6 +382,7 @@ 61D6FF7E24E53D3B00D0E375 /* BenchmarkMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */; }; 61D980BA24E28D0100E03345 /* RUMIntegrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D980B924E28D0100E03345 /* RUMIntegrations.swift */; }; 61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D980BB24E293F600E03345 /* RUMIntegrationsTests.swift */; }; + 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */; }; 61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */; }; 61DC6D922539E3E300FFAA22 /* LoggingCommonAsserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DC6D912539E3E300FFAA22 /* LoggingCommonAsserts.swift */; }; 61DE332625C826E4008E3EC2 /* CrashReportingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DE332525C826E4008E3EC2 /* CrashReportingFeature.swift */; }; @@ -411,6 +412,7 @@ 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D02465423600E6C631 /* TracerConfiguration.swift */; }; 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */; }; 61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */; }; + 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */; }; 61EF7890257E289A00EDCCB3 /* DeleteAllDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EF788F257E289A00EDCCB3 /* DeleteAllDataMigratorTests.swift */; }; 61EF789B257E2B0200EDCCB3 /* DataMigratorBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EF789A257E2B0200EDCCB3 /* DataMigratorBenchmarkTests.swift */; }; 61EF78B1257E2E7A00EDCCB3 /* MoveDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EF78B0257E2E7A00EDCCB3 /* MoveDataMigrator.swift */; }; @@ -663,7 +665,7 @@ 61133BB12423979B00786299 /* DataUploadWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadWorker.swift; sourceTree = ""; }; 61133BB22423979B00786299 /* HTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 61133BB32423979B00786299 /* DataUploadDelay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadDelay.swift; sourceTree = ""; }; - 61133BB42423979B00786299 /* HTTPHeaders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeaders.swift; sourceTree = ""; }; + 61133BB42423979B00786299 /* RequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; 61133BB52423979B00786299 /* DatadogConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogConfiguration.swift; sourceTree = ""; }; 61133BB62423979B00786299 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 61133BB82423979B00786299 /* InternalLoggers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalLoggers.swift; sourceTree = ""; }; @@ -704,7 +706,7 @@ 61133C302423990D00786299 /* DataUploadConditionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadConditionsTests.swift; sourceTree = ""; }; 61133C312423990D00786299 /* DataUploadDelayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadDelayTests.swift; sourceTree = ""; }; 61133C322423990D00786299 /* DataUploaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploaderTests.swift; sourceTree = ""; }; - 61133C332423990D00786299 /* HTTPHeadersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeadersTests.swift; sourceTree = ""; }; + 61133C332423990D00786299 /* RequestBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBuilderTests.swift; sourceTree = ""; }; 61133C342423990D00786299 /* HTTPClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClientTests.swift; sourceTree = ""; }; 61133C362423990D00786299 /* InternalLoggersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalLoggersTests.swift; sourceTree = ""; }; 61133C382423990D00786299 /* LoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; @@ -1008,6 +1010,7 @@ 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkMocks.swift; sourceTree = ""; }; 61D980B924E28D0100E03345 /* RUMIntegrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMIntegrations.swift; sourceTree = ""; }; 61D980BB24E293F600E03345 /* RUMIntegrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMIntegrationsTests.swift; sourceTree = ""; }; + 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatusTests.swift; sourceTree = ""; }; 61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomObjcViewController.h; sourceTree = ""; }; 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomObjcViewController.m; sourceTree = ""; }; 61DC6D912539E3E300FFAA22 /* LoggingCommonAsserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingCommonAsserts.swift; sourceTree = ""; }; @@ -1037,6 +1040,7 @@ 61E917D02465423600E6C631 /* TracerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfiguration.swift; sourceTree = ""; }; 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfigurationTests.swift; sourceTree = ""; }; 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportExporterTests.swift; sourceTree = ""; }; + 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatus.swift; sourceTree = ""; }; 61EF788F257E289A00EDCCB3 /* DeleteAllDataMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllDataMigratorTests.swift; sourceTree = ""; }; 61EF789A257E2B0200EDCCB3 /* DataMigratorBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigratorBenchmarkTests.swift; sourceTree = ""; }; 61EF78B0257E2E7A00EDCCB3 /* MoveDataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveDataMigrator.swift; sourceTree = ""; }; @@ -1418,10 +1422,11 @@ children = ( 61133BAF2423979B00786299 /* DataUploadConditions.swift */, 61133BB32423979B00786299 /* DataUploadDelay.swift */, + 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */, 61133BB02423979B00786299 /* DataUploader.swift */, 61133BB12423979B00786299 /* DataUploadWorker.swift */, 61133BB22423979B00786299 /* HTTPClient.swift */, - 61133BB42423979B00786299 /* HTTPHeaders.swift */, + 61133BB42423979B00786299 /* RequestBuilder.swift */, ); path = Upload; sourceTree = ""; @@ -1668,9 +1673,10 @@ 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */, 61133C302423990D00786299 /* DataUploadConditionsTests.swift */, 61133C312423990D00786299 /* DataUploadDelayTests.swift */, + 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */, 61133C322423990D00786299 /* DataUploaderTests.swift */, - 61133C332423990D00786299 /* HTTPHeadersTests.swift */, 61133C342423990D00786299 /* HTTPClientTests.swift */, + 61133C332423990D00786299 /* RequestBuilderTests.swift */, ); path = Upload; sourceTree = ""; @@ -3805,8 +3811,9 @@ 6161249E25CAB340009901BE /* CrashContext.swift in Sources */, 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */, 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */, - 61133BDA2423979B00786299 /* HTTPHeaders.swift in Sources */, + 61133BDA2423979B00786299 /* RequestBuilder.swift in Sources */, 61C3E63924BF19B4008053F2 /* RUMContext.swift in Sources */, + 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */, 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, @@ -3914,7 +3921,7 @@ 613F23F1252B129E006CD2D7 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, 61B03879252724AB00518F3C /* URLSessionInterceptorTests.swift in Sources */, 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */, - 61133C602423990D00786299 /* HTTPHeadersTests.swift in Sources */, + 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */, 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */, 61D6FF7924E42A2900D0E375 /* DataUploadWorkerMock.swift in Sources */, @@ -3961,6 +3968,7 @@ 61A9238E256FCAA2009B9667 /* DateCorrectionTests.swift in Sources */, 61EF7890257E289A00EDCCB3 /* DeleteAllDataMigratorTests.swift in Sources */, 61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */, + 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */, 614AD086254C3027004999A3 /* LaunchTimeProviderTests.swift in Sources */, 6139CD772589FEE3007E8BB7 /* RetryingTests.swift in Sources */, 61133C482423990D00786299 /* DDDatadogTests.swift in Sources */, diff --git a/Sources/Datadog/Core/Feature.swift b/Sources/Datadog/Core/Feature.swift index ba45cd9ce0..2a00d27da0 100644 --- a/Sources/Datadog/Core/Feature.swift +++ b/Sources/Datadog/Core/Feature.swift @@ -143,8 +143,7 @@ internal struct FeatureUpload { init( featureName: String, storage: FeatureStorage, - uploadHTTPHeaders: HTTPHeaders, - uploadURLProvider: UploadURLProvider, + requestBuilder: RequestBuilder, commonDependencies: FeaturesCommonDependencies, internalMonitor: InternalMonitor? = nil ) { @@ -159,10 +158,8 @@ internal struct FeatureUpload { ) let dataUploader = DataUploader( - urlProvider: uploadURLProvider, httpClient: commonDependencies.httpClient, - httpHeaders: uploadHTTPHeaders, - internalMonitor: internalMonitor + requestBuilder: requestBuilder ) self.init( @@ -172,7 +169,8 @@ internal struct FeatureUpload { dataUploader: dataUploader, uploadConditions: uploadConditions, delay: DataUploadDelay(performance: commonDependencies.performance), - featureName: featureName + featureName: featureName, + internalMonitor: internalMonitor ) ) } diff --git a/Sources/Datadog/Core/FeaturesConfiguration.swift b/Sources/Datadog/Core/FeaturesConfiguration.swift index 52fd9435c7..4b491eca26 100644 --- a/Sources/Datadog/Core/FeaturesConfiguration.swift +++ b/Sources/Datadog/Core/FeaturesConfiguration.swift @@ -23,12 +23,14 @@ internal struct FeaturesConfiguration { struct Logging { let common: Common - let uploadURLWithClientToken: URL + let uploadURL: URL + let clientToken: String } struct Tracing { let common: Common - let uploadURLWithClientToken: URL + let uploadURL: URL + let clientToken: String let spanEventMapper: SpanEventMapper? } @@ -39,7 +41,8 @@ internal struct FeaturesConfiguration { } let common: Common - let uploadURLWithClientToken: URL + let uploadURL: URL + let clientToken: String let applicationID: String let sessionSamplingRate: Float let viewEventMapper: RUMViewEventMapper? @@ -76,7 +79,9 @@ internal struct FeaturesConfiguration { let sdkEnvironment: String /// Internal monitoring logger's name. let loggerName = "im-logger" - let logsUploadURLWithClientToken: URL + let logsUploadURL: URL + /// The client token authorized for monitoring org (likely it's different than client token for other features). + let clientToken: String } /// Configuration common to all features. @@ -157,20 +162,16 @@ extension FeaturesConfiguration { if configuration.loggingEnabled { logging = Logging( common: common, - uploadURLWithClientToken: try ifValid( - endpointURLString: logsEndpoint.url, - clientToken: configuration.clientToken - ) + uploadURL: try ifValid(endpointURLString: logsEndpoint.url), + clientToken: try ifValid(clientToken: configuration.clientToken) ) } if configuration.tracingEnabled { tracing = Tracing( common: common, - uploadURLWithClientToken: try ifValid( - endpointURLString: tracesEndpoint.url, - clientToken: configuration.clientToken - ), + uploadURL: try ifValid(endpointURLString: tracesEndpoint.url), + clientToken: try ifValid(clientToken: configuration.clientToken), spanEventMapper: configuration.spanEventMapper ) } @@ -188,10 +189,8 @@ extension FeaturesConfiguration { if let rumApplicationID = configuration.rumApplicationID { rum = RUM( common: common, - uploadURLWithClientToken: try ifValid( - endpointURLString: rumEndpoint.url, - clientToken: configuration.clientToken - ), + uploadURL: try ifValid(endpointURLString: rumEndpoint.url), + clientToken: try ifValid(clientToken: configuration.clientToken), applicationID: rumApplicationID, sessionSamplingRate: configuration.rumSessionsSamplingRate, viewEventMapper: configuration.rumViewEventMapper, @@ -263,10 +262,8 @@ extension FeaturesConfiguration { common: common, sdkServiceName: "dd-sdk-ios", sdkEnvironment: "prod", - logsUploadURLWithClientToken: try ifValid( - endpointURLString: Datadog.Configuration.DatadogEndpoint.us1.logsEndpoint.url, - clientToken: internalMonitoringClientToken - ) + logsUploadURL: try ifValid(endpointURLString: Datadog.Configuration.DatadogEndpoint.us1.logsEndpoint.url), + clientToken: try ifValid(clientToken: internalMonitoringClientToken) ) } @@ -291,18 +288,18 @@ private func ifValid(environment: String) throws -> String { return environment } -private func ifValid(endpointURLString: String, clientToken: String) throws -> URL { +private func ifValid(endpointURLString: String) throws -> URL { guard let endpointURL = URL(string: endpointURLString) else { throw ProgrammerError(description: "The `url` in `.custom(url:)` must be a valid URL string.") } + return endpointURL +} + +private func ifValid(clientToken: String) throws -> String { if clientToken.isEmpty { throw ProgrammerError(description: "`clientToken` cannot be empty.") } - let endpointURLWithClientToken = endpointURL.appendingPathComponent(clientToken) - guard let url = URL(string: endpointURLWithClientToken.absoluteString) else { - throw ProgrammerError(description: "Cannot build upload URL.") - } - return url + return clientToken } private func sanitized(firstPartyHosts: Set) -> Set { diff --git a/Sources/Datadog/Core/Upload/DataUploadStatus.swift b/Sources/Datadog/Core/Upload/DataUploadStatus.swift new file mode 100644 index 0000000000..fef7923936 --- /dev/null +++ b/Sources/Datadog/Core/Upload/DataUploadStatus.swift @@ -0,0 +1,164 @@ +/* + * 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 Foundation + +private enum HTTPResponseStatusCode: Int { + /// The request has been accepted for processing. + case accepted = 202 + /// The server cannot or will not process the request (client error). + case badRequest = 400 + /// The request lacks valid authentication credentials. + case unauthorized = 401 + /// The server understood the request but refuses to authorize it. + case forbidden = 403 + /// The server would like to shut down the connection. + case requestTimeout = 408 + /// The request entity is larger than limits defined by server. + case payloadTooLarge = 413 + /// The client has sent too many requests in a given amount of time. + case tooManyRequests = 429 + /// The server encountered an unexpected condition. + case internalServerError = 500 + /// The server is not ready to handle the request probably because it is overloaded. + case serviceUnavailable = 503 + /// An unexpected status code. + case unexpected = -999 + + /// If it makes sense to retry the upload finished with this status code, e.g. if data upload failed due to `503` HTTP error, we should retry it later. + var needsRetry: Bool { + switch self { + case .accepted, .badRequest, .unauthorized, .forbidden, .payloadTooLarge: + // No retry - it's either success or a client error which won't be fixed in next upload. + return false + case .requestTimeout, .tooManyRequests, .internalServerError, .serviceUnavailable: + // Retry - it's a temporary server or connection issue that might disappear on next attempt. + return true + case .unexpected: + // This shouldn't happen, but if receiving an unexpected status code we do not retry. + // This is safer than retrying as we don't know if the issue is coming from the client or server. + return false + } + } +} + +/// The status of a single upload attempt. +internal struct DataUploadStatus { + /// If upload needs to be retried (`true`) because its associated data was not delivered but it may succeed + /// in the next attempt (i.e. it failed due to device leaving signal range or a temporary server unavailability occured). + /// If set to `false` then data associated with the upload should be deleted as it does not need any more upload + /// attempts (i.e. the upload succeeded or failed due to unrecoverable client error). + let needsRetry: Bool + + // MARK: - Debug Info + + /// Upload status description printed to the console if SDK `.debug` verbosity is enabled. + let userDebugDescription: String + + /// An optional error printed to the console if SDK `.error` (or lower) verbosity is enabled. + /// It is meant to indicate user action that must be taken to fix the upload issue (e.g. if the client token is invalid, it needs to be fixed). + let userErrorMessage: String? + + /// An optional error logged to the Internal Monitoring feature (if it's enabled). + let internalMonitoringError: (message: String, error: Error?, attributes: [String: String]?)? +} + +extension DataUploadStatus { + // MARK: - Initialization + + init(httpResponse: HTTPURLResponse, ddRequestID: String?) { + let statusCode = HTTPResponseStatusCode(rawValue: httpResponse.statusCode) ?? .unexpected + + self.init( + needsRetry: statusCode.needsRetry, + userDebugDescription: "[response code: \(httpResponse.statusCode) (\(statusCode)), request ID: \(ddRequestID ?? "(???)")]", + userErrorMessage: statusCode == .unauthorized ? "⚠️ The client token you provided seems to be invalid." : nil, + internalMonitoringError: createInternalMonitoringErrorIfNeeded(for: httpResponse.statusCode, requestID: ddRequestID) + ) + } + + init(networkError: Error) { + self.init( + needsRetry: true, // retry this upload as it failed due to network transport isse + userDebugDescription: "[error: \(DDError(error: networkError).message)]", // e.g. "[error: A data connection is not currently allowed]" + userErrorMessage: nil, // nothing actionable for the user + internalMonitoringError: createInternalMonitoringErrorIfNeeded(for: networkError) + ) + } +} + +// MARK: - Internal Monitoring + +#if DD_SDK_ENABLE_INTERNAL_MONITORING +/// Looks at the `statusCode` and produces error for Internal Monitoring feature if anything is going wrong. +private func createInternalMonitoringErrorIfNeeded( + for statusCode: Int, requestID: String? +) -> (message: String, error: Error?, attributes: [String: String]?)? { + guard let responseStatusCode = HTTPResponseStatusCode(rawValue: statusCode) else { + // If status code is unexpected, do not produce an error for Internal Monitoring - otherwise monitoring may + // become too verbose for old installations if we introduce a new status code in the API. + return nil + } + + switch responseStatusCode { + case .accepted, .unauthorized, .forbidden: + // These codes mean either success or the user configuration mistake - do not produce error. + return nil + case .internalServerError, .serviceUnavailable: + // These codes mean Datadog service issue - do not produce SDK error as this is already monitored by other means. + return nil + case .badRequest, .payloadTooLarge, .tooManyRequests, .requestTimeout: + // These codes mean that something wrong is happening either in the SDK or on the server - produce an error. + return ( + message: "Data upload finished with status code: \(statusCode)", + error: nil, + attributes: ["dd_request_id": requestID ?? "(???)"] + ) + case .unexpected: + return nil + } +} + +/// A list of known NSURLError codes which should not produce error in Internal Monitoring. +/// Receiving these codes doesn't mean SDK issue, but the network transportation scenario where the connection interrupted due to external factors. +/// These list should evolve and we may want to add more codes in there. +/// +/// Ref.: https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes +private let ignoredNSURLErrorCodes = Set([ + NSURLErrorNetworkConnectionLost, // -1005 + NSURLErrorTimedOut, // -1001 + NSURLErrorCannotParseResponse, // - 1017 + NSURLErrorNotConnectedToInternet, // -1009 + NSURLErrorCannotFindHost, // -1003 + NSURLErrorSecureConnectionFailed, // -1200 + NSURLErrorDataNotAllowed, // -1020 + NSURLErrorCannotConnectToHost, // -1004 +]) + +/// Looks at the `networkError` and produces error for Internal Monitoring feature if anything is going wrong. +private func createInternalMonitoringErrorIfNeeded( + for networkError: Error +) -> (message: String, error: Error?, attributes: [String: String]?)? { + let nsError = networkError as NSError + if nsError.domain == NSURLErrorDomain && !ignoredNSURLErrorCodes.contains(nsError.code) { + return (message: "Data upload finished with error", error: nsError, attributes: nil) + } else { + return nil + } +} +#else +private func createInternalMonitoringErrorIfNeeded( + for statusCode: Int, requestID: String? +) -> (message: String, error: Error?, attributes: [String: String]?)? { + return nil +} + +private func createInternalMonitoringErrorIfNeeded( + for networkError: Error +) -> (message: String, error: Error?, attributes: [String: String]?)? { + return nil +} +#endif diff --git a/Sources/Datadog/Core/Upload/DataUploadWorker.swift b/Sources/Datadog/Core/Upload/DataUploadWorker.swift index c1ee47a82c..5d15e149f0 100644 --- a/Sources/Datadog/Core/Upload/DataUploadWorker.swift +++ b/Sources/Datadog/Core/Upload/DataUploadWorker.swift @@ -20,16 +20,13 @@ internal class DataUploadWorker: DataUploadWorkerType { /// File reader providing data to upload. private let fileReader: Reader /// Data uploader sending data to server. - private let dataUploader: DataUploader + private let dataUploader: DataUploaderType /// Variable system conditions determining if upload should be performed. private let uploadConditions: DataUploadConditions - /// For each file upload, the status is checked against this list of acceptable statuses. - /// If it's there, the file will be deleted. If not, it will be retried in next upload. - private let acceptableUploadStatuses: Set = [ - .success, .redirection, .clientError, .clientTokenError, .unknown - ] /// Name of the feature this worker is performing uploads for. private let featureName: String + /// A monitor reporting errors through Internal Monitoring feature (if enabled). + private let internalMonitor: InternalMonitor? /// Delay used to schedule consecutive uploads. private var delay: Delay @@ -40,10 +37,11 @@ internal class DataUploadWorker: DataUploadWorkerType { init( queue: DispatchQueue, fileReader: Reader, - dataUploader: DataUploader, + dataUploader: DataUploaderType, uploadConditions: DataUploadConditions, delay: Delay, - featureName: String + featureName: String, + internalMonitor: InternalMonitor? = nil ) { self.queue = queue self.fileReader = fileReader @@ -51,6 +49,7 @@ internal class DataUploadWorker: DataUploadWorkerType { self.dataUploader = dataUploader self.delay = delay self.featureName = featureName + self.internalMonitor = internalMonitor let uploadWork = DispatchWorkItem { [weak self] in guard let self = self else { @@ -63,18 +62,29 @@ internal class DataUploadWorker: DataUploadWorkerType { if let batch = nextBatch { userLogger.debug("⏳ (\(self.featureName)) Uploading batch...") + // Upload batch let uploadStatus = self.dataUploader.upload(data: batch.data) - let shouldBeAccepted = self.acceptableUploadStatuses.contains(uploadStatus) - if shouldBeAccepted { + // Delete or keep batch depending on the upload status + if uploadStatus.needsRetry { + self.delay.increase() + + userLogger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") + } else { self.fileReader.markBatchAsRead(batch) self.delay.decrease() - userLogger.debug(" → (\(self.featureName)) accepted, won't be retransmitted: \(uploadStatus)") - } else { - self.delay.increase() + userLogger.debug(" → (\(self.featureName)) accepted, won't be retransmitted: \(uploadStatus.userDebugDescription)") + } + + // Print user error (if any) + if let userErrorMessage = uploadStatus.userErrorMessage { + userLogger.error(userErrorMessage) + } - userLogger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus)") + // Send internal monitoring error (if any and if Internal Monitoring is enabled) + if let sdkError = uploadStatus.internalMonitoringError { + self.internalMonitor?.sdkLogger.error(sdkError.message, error: sdkError.error, attributes: sdkError.attributes) } } else { let batchLabel = nextBatch != nil ? "YES" : (isSystemReady ? "NO" : "NOT CHECKED") diff --git a/Sources/Datadog/Core/Upload/DataUploader.swift b/Sources/Datadog/Core/Upload/DataUploader.swift index 95e2428056..aa8a1ca6c4 100644 --- a/Sources/Datadog/Core/Upload/DataUploader.swift +++ b/Sources/Datadog/Core/Upload/DataUploader.swift @@ -6,88 +6,38 @@ import Foundation -/// Creates URL and adds query items before providing them -internal class UploadURLProvider { - private let urlWithClientToken: URL - private let queryItemProviders: [QueryItemProvider] - - class QueryItemProvider { - let value: () -> URLQueryItem - - /// Creates `ddsource=ios` query item. - static func ddsource(source: String) -> QueryItemProvider { - let queryItem = URLQueryItem(name: "ddsource", value: source) - return QueryItemProvider { queryItem } - } - - /// Creates `ddtags=tag1,tag2,...` query item. - static func ddtags(tags: [String]) -> QueryItemProvider { - let queryItem = URLQueryItem(name: "ddtags", value: tags.joined(separator: ",")) - return QueryItemProvider { queryItem } - } - - private init(value: @escaping () -> URLQueryItem) { - self.value = value - } - } - - var url: URL { - // In RUMM-655 we've removed the last dynamic query item and this `url` may just become constant - // in the future. - - var urlComponents = URLComponents(url: urlWithClientToken, resolvingAgainstBaseURL: false) - - if !queryItemProviders.isEmpty { - urlComponents?.queryItems = queryItemProviders.map { $0.value() } - } - - guard let url = urlComponents?.url else { - userLogger.error("🔥 Failed to create URL from \(urlWithClientToken) with \(queryItemProviders)") - return urlWithClientToken - } - return url - } - - init(urlWithClientToken: URL, queryItemProviders: [QueryItemProvider]) { - self.urlWithClientToken = urlWithClientToken - self.queryItemProviders = queryItemProviders - } +/// A type that performs data uploads. +internal protocol DataUploaderType { + func upload(data: Data) -> DataUploadStatus } /// Synchronously uploads data to server using `HTTPClient`. -internal final class DataUploader { - private let urlProvider: UploadURLProvider +internal final class DataUploader: DataUploaderType { + /// An unreachable upload status - only meant to satisfy the compiler. + private static let unreachableUploadStatus = DataUploadStatus(needsRetry: false, userDebugDescription: "", userErrorMessage: nil, internalMonitoringError: nil) + private let httpClient: HTTPClient - private let httpHeaders: HTTPHeaders - private let internalMonitor: InternalMonitor? + private let requestBuilder: RequestBuilder - init( - urlProvider: UploadURLProvider, - httpClient: HTTPClient, - httpHeaders: HTTPHeaders, - internalMonitor: InternalMonitor? = nil - ) { - self.urlProvider = urlProvider + init(httpClient: HTTPClient, requestBuilder: RequestBuilder) { self.httpClient = httpClient - self.httpHeaders = httpHeaders - self.internalMonitor = internalMonitor + self.requestBuilder = requestBuilder } - /// Uploads data synchronously (will block current thread) and returns upload status. + /// Uploads data synchronously (will block current thread) and returns the upload status. /// Uses timeout configured for `HTTPClient`. func upload(data: Data) -> DataUploadStatus { - let request = createRequestWith(data: data) + let (request, ddRequestID) = createRequest(with: data) var uploadStatus: DataUploadStatus? let semaphore = DispatchSemaphore(value: 0) - httpClient.send(request: request) { [weak self] result in + httpClient.send(request: request) { result in switch result { case .success(let httpResponse): - uploadStatus = DataUploadStatus(from: httpResponse) + uploadStatus = DataUploadStatus(httpResponse: httpResponse, ddRequestID: ddRequestID) case .failure(let error): - self?.internalMonitor?.sdkLogger.error("Failed to upload data", error: error) - uploadStatus = .networkError + uploadStatus = DataUploadStatus(networkError: error) } semaphore.signal() @@ -95,43 +45,12 @@ internal final class DataUploader { _ = semaphore.wait(timeout: .distantFuture) - return uploadStatus ?? .unknown + return uploadStatus ?? DataUploader.unreachableUploadStatus } - private func createRequestWith(data: Data) -> URLRequest { - var request = URLRequest(url: urlProvider.url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = httpHeaders.all - request.httpBody = data - return request - } -} - -internal enum DataUploadStatus: Equatable, Hashable { - /// Corresponds to HTTP 2xx response status codes. - case success - /// Corresponds to HTTP 3xx response status codes. - case redirection - /// Corresponds to HTTP 403 response status codes, - /// which means client token is invalid - case clientTokenError - /// Corresponds to HTTP 4xx response status codes. - case clientError - /// Corresponds to HTTP 5xx response status codes. - case serverError - /// Means transportation error and no delivery at all. - case networkError - /// Corresponds to unknown HTTP response status code. - case unknown - - init(from httpResponse: HTTPURLResponse) { - switch httpResponse.statusCode { - case 200...299: self = .success - case 300...399: self = .redirection - case 403: self = .clientTokenError - case 400...499: self = .clientError - case 500...599: self = .serverError - default: self = .unknown - } + private func createRequest(with data: Data) -> (request: URLRequest, ddRequestID: String?) { + let request = requestBuilder.uploadRequest(with: data) + let requestID = request.value(forHTTPHeaderField: RequestBuilder.HTTPHeader.ddRequestIDHeaderField) + return (request: request, ddRequestID: requestID) } } diff --git a/Sources/Datadog/Core/Upload/HTTPHeaders.swift b/Sources/Datadog/Core/Upload/HTTPHeaders.swift deleted file mode 100644 index b2c08f9a8f..0000000000 --- a/Sources/Datadog/Core/Upload/HTTPHeaders.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import Foundation - -/// HTTP headers associated with requests send by SDK. -internal struct HTTPHeaders { - enum ContentType: String { - case applicationJSON = "application/json" - case textPlainUTF8 = "text/plain;charset=UTF-8" - } - - struct HTTPHeader { - let field: String - let value: String - - // MARK: - Supported headers - - static func contentTypeHeader(contentType: ContentType) -> HTTPHeader { - return HTTPHeader(field: "Content-Type", value: contentType.rawValue) - } - - static func userAgentHeader(appName: String, appVersion: String, device: MobileDevice) -> HTTPHeader { - return HTTPHeader( - field: "User-Agent", - value: "\(appName)/\(appVersion) CFNetwork (\(device.model); \(device.osName)/\(device.osVersion))" - ) - } - - // MARK: - Initialization - - private init(field: String, value: String) { - self.field = field - self.value = value - } - } - - let all: [String: String] - - init(headers: [HTTPHeader]) { - self.all = headers.reduce([:]) { acc, next in - var dictionary = acc - dictionary[next.field] = next.value - return dictionary - } - } -} diff --git a/Sources/Datadog/Core/Upload/RequestBuilder.swift b/Sources/Datadog/Core/Upload/RequestBuilder.swift new file mode 100644 index 0000000000..5de90d6465 --- /dev/null +++ b/Sources/Datadog/Core/Upload/RequestBuilder.swift @@ -0,0 +1,134 @@ +/* + * 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 Foundation + +/// Builds `URLRequest` for sending data to Datadog. +internal struct RequestBuilder { + enum QueryItem { + /// `ddsource={source}` query item + case ddsource(source: String) + /// `ddtags={tag1},{tag2},...` query item + case ddtags(tags: [String]) + + var urlQueryItem: URLQueryItem { + switch self { + case .ddsource(let source): + return URLQueryItem(name: "ddsource", value: source) + case .ddtags(let tags): + return URLQueryItem(name: "ddtags", value: tags.joined(separator: ",")) + } + } + } + + enum ContentType: String { + case applicationJSON = "application/json" + case textPlainUTF8 = "text/plain;charset=UTF-8" + } + + struct HTTPHeader { + static let contentTypeHeaderField = "Content-Type" + static let userAgentHeaderField = "User-Agent" + static let ddAPIKeyHeaderField = "DD-API-KEY" + static let ddEVPOriginHeaderField = "DD-EVP-ORIGIN" + static let ddEVPOriginVersionHeaderField = "DD-EVP-ORIGIN-VERSION" + static let ddRequestIDHeaderField = "DD-REQUEST-ID" + + enum Value { + /// If the header's value is constant. + case constant(_ value: String) + /// If the header's value is different each time. + case dynamic(_ value: () -> String) + } + + let field: String + let value: Value + + // MARK: - Standard Headers + + /// Standard "Content-Type" header. + static func contentTypeHeader(contentType: ContentType) -> HTTPHeader { + return HTTPHeader(field: contentTypeHeaderField, value: .constant(contentType.rawValue)) + } + + /// Standard "User-Agent" header. + static func userAgentHeader(appName: String, appVersion: String, device: MobileDevice) -> HTTPHeader { + return HTTPHeader( + field: userAgentHeaderField, + value: .constant("\(appName)/\(appVersion) CFNetwork (\(device.model); \(device.osName)/\(device.osVersion))") + ) + } + + // MARK: - Datadog Headers + + /// Datadog request authentication header. + static func ddAPIKeyHeader(clientToken: String) -> HTTPHeader { + return HTTPHeader(field: ddAPIKeyHeaderField, value: .constant(clientToken)) + } + + /// An observability and troubleshooting Datadog header for tracking the origin which is sending the request. + static func ddEVPOriginHeader(source: String) -> HTTPHeader { + return HTTPHeader(field: ddEVPOriginHeaderField, value: .constant(source)) + } + + /// An observability and troubleshooting Datadog header for tracking the origin which is sending the request. + static func ddEVPOriginVersionHeader() -> HTTPHeader { + return HTTPHeader(field: ddEVPOriginVersionHeaderField, value: .constant(sdkVersion)) + } + + /// An optional Datadog header for debugging Intake requests by their ID. + static func ddRequestIDHeader() -> HTTPHeader { + return HTTPHeader(field: ddRequestIDHeaderField, value: .dynamic({ UUID().uuidString })) + } + } + + /// Upload `URL`. + private let url: URL + /// Pre-computed HTTP headers (they do not change in succeeding requests). + private let precomputedHeaders: [String: String] + /// Computed HTTP headers (their value is different in succeeding requests). + private let computedHeaders: [String: () -> String] + + // MARK: - Initialization + + init(url: URL, queryItems: [QueryItem], headers: [HTTPHeader]) { + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if !queryItems.isEmpty { + urlComponents?.queryItems = queryItems.map { $0.urlQueryItem } + } + + var precomputedHeaders: [String: String] = [:] + var computedHeaders: [String: () -> String] = [:] + headers.forEach { header in + switch header.value { + case .constant(let value): + precomputedHeaders[header.field] = value + case .dynamic(let value): + computedHeaders[header.field] = value + } + } + + self.url = urlComponents?.url ?? url + self.precomputedHeaders = precomputedHeaders + self.computedHeaders = computedHeaders + } + + /// Creates `URLRequest` for uploading given `data` to Datadog. + /// - Parameter data: data to be uploaded + /// - Returns: the `URLRequest` object and `DD-REQUEST-ID` header value (for debugging). + func uploadRequest(with data: Data) -> URLRequest { + var request = URLRequest(url: url) + var headers = precomputedHeaders + computedHeaders.forEach { field, value in headers[field] = value() } + + request.httpMethod = "POST" + request.allHTTPHeaderFields = headers + request.httpBody = data + + return request + } +} diff --git a/Sources/Datadog/DatadogConfiguration.swift b/Sources/Datadog/DatadogConfiguration.swift index 832341f427..5db1050128 100644 --- a/Sources/Datadog/DatadogConfiguration.swift +++ b/Sources/Datadog/DatadogConfiguration.swift @@ -116,11 +116,12 @@ extension Datadog { case custom(url: String) internal var url: String { + let endpoint = "api/v2/logs" switch self { - case .us1, .us: return "https://logs.browser-intake-datadoghq.com/v1/input/" - case .us3: return "https://logs.browser-intake-us3-datadoghq.com/v1/input/" - case .eu1, .eu: return "https://mobile-http-intake.logs.datadoghq.eu/v1/input/" - case .us1_fed, .gov: return "https://logs.browser-intake-ddog-gov.com/v1/input/" + case .us1, .us: return "https://logs.browser-intake-datadoghq.com/" + endpoint + case .us3: return "https://logs.browser-intake-us3-datadoghq.com/" + endpoint + case .eu1, .eu: return "https://mobile-http-intake.logs.datadoghq.eu/" + endpoint + case .us1_fed, .gov: return "https://logs.browser-intake-ddog-gov.com/" + endpoint case let .custom(url: url): return url } } @@ -153,11 +154,12 @@ extension Datadog { case custom(url: String) internal var url: String { + let endpoint = "api/v2/spans" switch self { - case .us1, .us: return "https://trace.browser-intake-datadoghq.com/v1/input/" - case .us3: return "https://trace.browser-intake-us3-datadoghq.com/v1/input/" - case .eu1, .eu: return "https:/public-trace-http-intake.logs.datadoghq.eu/v1/input/" - case .us1_fed, .gov: return "https://trace.browser-intake-ddog-gov.com/v1/input/" + case .us1, .us: return "https://trace.browser-intake-datadoghq.com/" + endpoint + case .us3: return "https://trace.browser-intake-us3-datadoghq.com/" + endpoint + case .eu1, .eu: return "https:/public-trace-http-intake.logs.datadoghq.eu/" + endpoint + case .us1_fed, .gov: return "https://trace.browser-intake-ddog-gov.com/" + endpoint case let .custom(url: url): return url } } @@ -190,11 +192,12 @@ extension Datadog { case custom(url: String) internal var url: String { + let endpoint = "api/v2/rum" switch self { - case .us1, .us: return "https://rum.browser-intake-datadoghq.com/v1/input/" - case .us3: return "https://rum.browser-intake-us3-datadoghq.com/v1/input/" - case .eu1, .eu: return "https://rum-http-intake.logs.datadoghq.eu/v1/input/" - case .us1_fed, .gov: return "https://rum.browser-intake-ddog-gov.com/v1/input/" + case .us1, .us: return "https://rum.browser-intake-datadoghq.com/" + endpoint + case .us3: return "https://rum.browser-intake-us3-datadoghq.com/" + endpoint + case .eu1, .eu: return "https://rum-http-intake.logs.datadoghq.eu/" + endpoint + case .us1_fed, .gov: return "https://rum.browser-intake-ddog-gov.com/" + endpoint case let .custom(url: url): return url } } diff --git a/Sources/Datadog/InternalMonitoring/InternalMonitoringFeature.swift b/Sources/Datadog/InternalMonitoring/InternalMonitoringFeature.swift index b645a2f4cd..9b216622e5 100644 --- a/Sources/Datadog/InternalMonitoring/InternalMonitoringFeature.swift +++ b/Sources/Datadog/InternalMonitoring/InternalMonitoringFeature.swift @@ -66,20 +66,22 @@ internal final class InternalMonitoringFeature { return FeatureUpload( featureName: InternalMonitoringFeature.featureName, storage: storage, - uploadHTTPHeaders: HTTPHeaders( + requestBuilder: RequestBuilder( + url: configuration.logsUploadURL, + queryItems: [ + .ddsource(source: configuration.common.source) + ], headers: [ .contentTypeHeader(contentType: .applicationJSON), .userAgentHeader( appName: configuration.common.applicationName, appVersion: configuration.common.applicationVersion, device: commonDependencies.mobileDevice - ) - ] - ), - uploadURLProvider: UploadURLProvider( - urlWithClientToken: configuration.logsUploadURLWithClientToken, - queryItemProviders: [ - .ddsource(source: configuration.common.source) + ), + .ddAPIKeyHeader(clientToken: configuration.clientToken), + .ddEVPOriginHeader(source: configuration.common.source), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader(), ] ), commonDependencies: commonDependencies, diff --git a/Sources/Datadog/Logging/LoggingFeature.swift b/Sources/Datadog/Logging/LoggingFeature.swift index d945c5d5cd..a5ead0a352 100644 --- a/Sources/Datadog/Logging/LoggingFeature.swift +++ b/Sources/Datadog/Logging/LoggingFeature.swift @@ -72,20 +72,22 @@ internal final class LoggingFeature { return FeatureUpload( featureName: LoggingFeature.featureName, storage: storage, - uploadHTTPHeaders: HTTPHeaders( + requestBuilder: RequestBuilder( + url: configuration.uploadURL, + queryItems: [ + .ddsource(source: configuration.common.source) + ], headers: [ .contentTypeHeader(contentType: .applicationJSON), .userAgentHeader( appName: configuration.common.applicationName, appVersion: configuration.common.applicationVersion, device: commonDependencies.mobileDevice - ) - ] - ), - uploadURLProvider: UploadURLProvider( - urlWithClientToken: configuration.uploadURLWithClientToken, - queryItemProviders: [ - .ddsource(source: configuration.common.source) + ), + .ddAPIKeyHeader(clientToken: configuration.clientToken), + .ddEVPOriginHeader(source: configuration.common.source), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader(), ] ), commonDependencies: commonDependencies, diff --git a/Sources/Datadog/RUM/RUMFeature.swift b/Sources/Datadog/RUM/RUMFeature.swift index 8fd4d2976e..0da8e5b1f4 100644 --- a/Sources/Datadog/RUM/RUMFeature.swift +++ b/Sources/Datadog/RUM/RUMFeature.swift @@ -80,19 +80,9 @@ internal final class RUMFeature { return FeatureUpload( featureName: RUMFeature.featureName, storage: storage, - uploadHTTPHeaders: HTTPHeaders( - headers: [ - .contentTypeHeader(contentType: .textPlainUTF8), - .userAgentHeader( - appName: configuration.common.applicationName, - appVersion: configuration.common.applicationVersion, - device: commonDependencies.mobileDevice - ) - ] - ), - uploadURLProvider: UploadURLProvider( - urlWithClientToken: configuration.uploadURLWithClientToken, - queryItemProviders: [ + requestBuilder: RequestBuilder( + url: configuration.uploadURL, + queryItems: [ .ddsource(source: configuration.common.source), .ddtags( tags: [ @@ -102,6 +92,18 @@ internal final class RUMFeature { "env:\(configuration.common.environment)" ] ) + ], + headers: [ + .contentTypeHeader(contentType: .textPlainUTF8), + .userAgentHeader( + appName: configuration.common.applicationName, + appVersion: configuration.common.applicationVersion, + device: commonDependencies.mobileDevice + ), + .ddAPIKeyHeader(clientToken: configuration.clientToken), + .ddEVPOriginHeader(source: configuration.common.source), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader(), ] ), commonDependencies: commonDependencies, diff --git a/Sources/Datadog/Tracing/TracingFeature.swift b/Sources/Datadog/Tracing/TracingFeature.swift index 068eb5cd28..c618d087ab 100644 --- a/Sources/Datadog/Tracing/TracingFeature.swift +++ b/Sources/Datadog/Tracing/TracingFeature.swift @@ -79,20 +79,22 @@ internal final class TracingFeature { return FeatureUpload( featureName: TracingFeature.featureName, storage: storage, - uploadHTTPHeaders: HTTPHeaders( + requestBuilder: RequestBuilder( + url: configuration.uploadURL, + queryItems: [], headers: [ .contentTypeHeader(contentType: .textPlainUTF8), .userAgentHeader( appName: configuration.common.applicationName, appVersion: configuration.common.applicationVersion, device: commonDependencies.mobileDevice - ) + ), + .ddAPIKeyHeader(clientToken: configuration.clientToken), + .ddEVPOriginHeader(source: configuration.common.source), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader(), ] ), - uploadURLProvider: UploadURLProvider( - urlWithClientToken: configuration.uploadURLWithClientToken, - queryItemProviders: [] - ), commonDependencies: commonDependencies, internalMonitor: internalMonitor ) diff --git a/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift index bc56aaf0a9..7272e84c2b 100644 --- a/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/DataUpload/DataUploaderBenchmarkTests.swift @@ -25,9 +25,8 @@ class DataUploaderBenchmarkTests: BenchmarkTests { /// `DataUploader` leaves no memory footprint (the memory peak after upload is less or equal `0kB`). func testUploadingDataToServer_leavesNoMemoryFootprint() throws { let dataUploader = DataUploader( - urlProvider: mockUploadURLProvider(), httpClient: HTTPClient(), - httpHeaders: HTTPHeaders(headers: []) + requestBuilder: RequestBuilder(url: .mockAny(), queryItems: [.ddtags(tags: ["foo:bar"])], headers: []) ) // `measure` runs 5 iterations @@ -41,11 +40,4 @@ class DataUploaderBenchmarkTests: BenchmarkTests { // This makes sure that no request data is leaked (e.g. due to internal caching). } } - - private func mockUploadURLProvider() -> UploadURLProvider { - return UploadURLProvider( - urlWithClientToken: server.obtainUniqueRecordingSession().recordingURL, - queryItemProviders: [.ddtags(tags: ["foo:bar"])] - ) - } } diff --git a/Tests/DatadogIntegrationTests/Scenarios/Logging/LoggingCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/Logging/LoggingCommonAsserts.swift index 710bbe79ba..2d83074d68 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/Logging/LoggingCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/Logging/LoggingCommonAsserts.swift @@ -21,29 +21,39 @@ extension LoggingCommonAsserts { requests.forEach { request in XCTAssertEqual(request.httpMethod, "POST") - // Example path here: `/36882784-420B-494F-910D-CBAC5897A309/ui-tests-client-token?ddsource=ios` - let pathRegex = #"^(.*)(/ui-tests-client-token\?ddsource=ios)$"# + // Example path here: `/36882784-420B-494F-910D-CBAC5897A309?ddsource=ios` + let pathRegex = #"^(.*)(\?ddsource=ios)$"# XCTAssertTrue( request.path.matches(regex: pathRegex), """ Request path doesn't match the expected regex. ✉️ path: \(request.path) - 🧪 expected regex: \(pathRegex) - """, - file: file, - line: line - ) - let expectedHeader = "Content-Type: application/json" - XCTAssertTrue( - request.httpHeaders.contains(expectedHeader), - """ - Request doesn't contain expected header. - ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) - 🧪 expected header: \(expectedHeader) + 🧪 expected regex: \(pathRegex) """, file: file, line: line ) + + let expectedHeadersRegexes = [ + #"^Content-Type: application/json$"#, + #"^User-Agent: Example/1.0 CFNetwork \([a-zA-Z ]+; iOS/[0-9.]+\)$"#, // e.g. "User-Agent: Example/1.0 CFNetwork (iPhone; iOS/14.5)" + #"^DD-API-KEY: ui-tests-client-token$"#, + #"^DD-EVP-ORIGIN: ios$"#, + #"^DD-EVP-ORIGIN-VERSION: [0-9].[0-9].[0-9]([-a-z0-9])*$"#, // e.g. "DD-EVP-ORIGIN-VERSION: 1.7.0-beta2" + #"^DD-REQUEST-ID: [0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}$"# // e.g. "DD-REQUEST-ID: 524A2616-D2AA-4FE5-BBD9-898D173BE658" + ] + expectedHeadersRegexes.forEach { expectedHeaderRegex in + XCTAssertTrue( + request.httpHeaders.contains { $0.matches(regex: expectedHeaderRegex) }, + """ + Request doesn't contain header matching expected regex. + ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) + 🧪 expected regex: '\(expectedHeaderRegex)' + """, + file: file, + line: line + ) + } } } } diff --git a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift index cd997e72a1..d987eaa27e 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/RUM/RUMCommonAsserts.swift @@ -21,8 +21,8 @@ extension RUMCommonAsserts { requests.forEach { request in XCTAssertEqual(request.httpMethod, "POST") - // Example path here: `/36882784-420B-494F-910D-CBAC5897A309/ui-tests-client-token?ddsource=ios&&ddtags=service:ui-tests-service-name,version:1.0,sdk_version:1.3.0-beta3,env:integration` - let pathRegex = #"^(.*)(\/ui-tests-client-token\?ddsource=ios&ddtags=service:ui-tests-service-name,version:1.0,sdk_version:)([0-9].[0-9].[0-9]([-a-z0-9])*)(,env:integration)$"# + // Example path here: `/36882784-420B-494F-910D-CBAC5897A309?ddsource=ios&&ddtags=service:ui-tests-service-name,version:1.0,sdk_version:1.3.0-beta3,env:integration` + let pathRegex = #"^(.*)(\?ddsource=ios&ddtags=service:ui-tests-service-name,version:1.0,sdk_version:)([0-9].[0-9].[0-9]([-a-z0-9])*)(,env:integration)$"# XCTAssertTrue( request.path.matches(regex: pathRegex), """ @@ -33,17 +33,27 @@ extension RUMCommonAsserts { file: file, line: line ) - let expectedHeader = "Content-Type: text/plain;charset=UTF-8" - XCTAssertTrue( - request.httpHeaders.contains(expectedHeader), - """ - Request doesn't contain expected header. - ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) - 🧪 expected header: \(expectedHeader) - """, - file: file, - line: line - ) + + let expectedHeadersRegexes = [ + #"^Content-Type: text/plain;charset=UTF-8$"#, + #"^User-Agent: Example/1.0 CFNetwork \([a-zA-Z ]+; iOS/[0-9.]+\)$"#, // e.g. "User-Agent: Example/1.0 CFNetwork (iPhone; iOS/14.5)" + #"^DD-API-KEY: ui-tests-client-token$"#, + #"^DD-EVP-ORIGIN: ios$"#, + #"^DD-EVP-ORIGIN-VERSION: [0-9].[0-9].[0-9]([-a-z0-9])*$"#, // e.g. "DD-EVP-ORIGIN-VERSION: 1.7.0-beta2" + #"^DD-REQUEST-ID: [0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}$"# // e.g. "DD-REQUEST-ID: 524A2616-D2AA-4FE5-BBD9-898D173BE658" + ] + expectedHeadersRegexes.forEach { expectedHeaderRegex in + XCTAssertTrue( + request.httpHeaders.contains { $0.matches(regex: expectedHeaderRegex) }, + """ + Request doesn't contain header matching expected regex. + ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) + 🧪 expected regex: '\(expectedHeaderRegex)' + """, + file: file, + line: line + ) + } } } } diff --git a/Tests/DatadogIntegrationTests/Scenarios/Tracing/TracingCommonAsserts.swift b/Tests/DatadogIntegrationTests/Scenarios/Tracing/TracingCommonAsserts.swift index 6d521f7484..7a527f6483 100644 --- a/Tests/DatadogIntegrationTests/Scenarios/Tracing/TracingCommonAsserts.swift +++ b/Tests/DatadogIntegrationTests/Scenarios/Tracing/TracingCommonAsserts.swift @@ -33,29 +33,37 @@ extension TracingCommonAsserts { requests.forEach { request in XCTAssertEqual(request.httpMethod, "POST") - // Example path here: `/36882784-420B-494F-910D-CBAC5897A309/ui-tests-client-token` - let pathRegex = #"^(.*)(/ui-tests-client-token)$"# - XCTAssertTrue( - request.path.matches(regex: pathRegex), + // Example path here: `/36882784-420B-494F-910D-CBAC5897A309` + XCTAssertFalse( + request.path.contains("?"), """ - Request path doesn't match the expected regex. + Request path must contain no query parameters. ✉️ path: \(request.path) - 🧪 expected regex: \(pathRegex) - """, - file: file, - line: line - ) - let expectedHeader = "Content-Type: text/plain;charset=UTF-8" - XCTAssertTrue( - request.httpHeaders.contains(expectedHeader), - """ - Request doesn't contain expected header. - ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) - 🧪 expected header: \(expectedHeader) """, file: file, line: line ) + + let expectedHeadersRegexes = [ + #"^Content-Type: text/plain;charset=UTF-8$"#, + #"^User-Agent: Example/1.0 CFNetwork \([a-zA-Z ]+; iOS/[0-9.]+\)$"#, // e.g. "User-Agent: Example/1.0 CFNetwork (iPhone; iOS/14.5)" + #"^DD-API-KEY: ui-tests-client-token$"#, + #"^DD-EVP-ORIGIN: ios$"#, + #"^DD-EVP-ORIGIN-VERSION: [0-9].[0-9].[0-9]([-a-z0-9])*$"#, // e.g. "DD-EVP-ORIGIN-VERSION: 1.7.0-beta2" + #"^DD-REQUEST-ID: [0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}$"# // e.g. "DD-REQUEST-ID: 524A2616-D2AA-4FE5-BBD9-898D173BE658" + ] + expectedHeadersRegexes.forEach { expectedHeaderRegex in + XCTAssertTrue( + request.httpHeaders.contains { $0.matches(regex: expectedHeaderRegex) }, + """ + Request doesn't contain header matching expected regex. + ✉️ request headers: \(request.httpHeaders.joined(separator: "\n")) + 🧪 expected regex: '\(expectedHeaderRegex)' + """, + file: file, + line: line + ) + } } } diff --git a/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift b/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift index 047aa115b9..03f965717a 100644 --- a/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift +++ b/Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift @@ -121,15 +121,23 @@ class FeaturesConfigurationTests: XCTestCase { } } + func testClientToken() throws { + let clientToken: String = .mockRandom(among: "abcdefgh") + let configuration = try createConfiguration(clientToken: clientToken) + + XCTAssertEqual(configuration.logging?.clientToken, clientToken) + XCTAssertEqual(configuration.tracing?.clientToken, clientToken) + XCTAssertEqual(configuration.rum?.clientToken, clientToken) + XCTAssertNotEqual(configuration.internalMonitoring?.clientToken, clientToken) + } + func testEndpoint() throws { - let clientToken: String = .mockRandom(among: "abcdef") let randomLogsEndpoint: Datadog.Configuration.LogsEndpoint = .mockRandom() let randomTracesEndpoint: Datadog.Configuration.TracesEndpoint = .mockRandom() let randomRUMEndpoint: Datadog.Configuration.RUMEndpoint = .mockRandom() func configuration(datadogEndpoint: Datadog.Configuration.DatadogEndpoint?) throws -> FeaturesConfiguration { try createConfiguration( - clientToken: clientToken, datadogEndpoint: datadogEndpoint, logsEndpoint: randomLogsEndpoint, tracesEndpoint: randomTracesEndpoint, @@ -140,105 +148,105 @@ class FeaturesConfigurationTests: XCTestCase { typealias DeprecatedEndpoints = Deprecated XCTAssertEqual( - try configuration(datadogEndpoint: .us1).logging?.uploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1).logging?.uploadURL.absoluteString, + "https://logs.browser-intake-datadoghq.com/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us3).logging?.uploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-us3-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us3).logging?.uploadURL.absoluteString, + "https://logs.browser-intake-us3-datadoghq.com/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: .eu1).logging?.uploadURLWithClientToken.absoluteString, - "https://mobile-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: .eu1).logging?.uploadURL.absoluteString, + "https://mobile-http-intake.logs.datadoghq.eu/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us1_fed).logging?.uploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1_fed).logging?.uploadURL.absoluteString, + "https://logs.browser-intake-ddog-gov.com/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.us).logging?.uploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.us).logging?.uploadURL.absoluteString, + "https://logs.browser-intake-datadoghq.com/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.eu).logging?.uploadURLWithClientToken.absoluteString, - "https://mobile-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.eu).logging?.uploadURL.absoluteString, + "https://mobile-http-intake.logs.datadoghq.eu/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.gov).logging?.uploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.gov).logging?.uploadURL.absoluteString, + "https://logs.browser-intake-ddog-gov.com/api/v2/logs" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us1).tracing?.uploadURLWithClientToken.absoluteString, - "https://trace.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1).tracing?.uploadURL.absoluteString, + "https://trace.browser-intake-datadoghq.com/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us3).tracing?.uploadURLWithClientToken.absoluteString, - "https://trace.browser-intake-us3-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us3).tracing?.uploadURL.absoluteString, + "https://trace.browser-intake-us3-datadoghq.com/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: .eu1).tracing?.uploadURLWithClientToken.absoluteString, - "https:/public-trace-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: .eu1).tracing?.uploadURL.absoluteString, + "https:/public-trace-http-intake.logs.datadoghq.eu/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us1_fed).tracing?.uploadURLWithClientToken.absoluteString, - "https://trace.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1_fed).tracing?.uploadURL.absoluteString, + "https://trace.browser-intake-ddog-gov.com/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.us).tracing?.uploadURLWithClientToken.absoluteString, - "https://trace.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.us).tracing?.uploadURL.absoluteString, + "https://trace.browser-intake-datadoghq.com/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.eu).tracing?.uploadURLWithClientToken.absoluteString, - "https:/public-trace-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.eu).tracing?.uploadURL.absoluteString, + "https:/public-trace-http-intake.logs.datadoghq.eu/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.gov).tracing?.uploadURLWithClientToken.absoluteString, - "https://trace.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.gov).tracing?.uploadURL.absoluteString, + "https://trace.browser-intake-ddog-gov.com/api/v2/spans" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us1).rum?.uploadURLWithClientToken.absoluteString, - "https://rum.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1).rum?.uploadURL.absoluteString, + "https://rum.browser-intake-datadoghq.com/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us3).rum?.uploadURLWithClientToken.absoluteString, - "https://rum.browser-intake-us3-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us3).rum?.uploadURL.absoluteString, + "https://rum.browser-intake-us3-datadoghq.com/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: .eu1).rum?.uploadURLWithClientToken.absoluteString, - "https://rum-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: .eu1).rum?.uploadURL.absoluteString, + "https://rum-http-intake.logs.datadoghq.eu/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: .us1_fed).rum?.uploadURLWithClientToken.absoluteString, - "https://rum.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: .us1_fed).rum?.uploadURL.absoluteString, + "https://rum.browser-intake-ddog-gov.com/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.us).rum?.uploadURLWithClientToken.absoluteString, - "https://rum.browser-intake-datadoghq.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.us).rum?.uploadURL.absoluteString, + "https://rum.browser-intake-datadoghq.com/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.eu).rum?.uploadURLWithClientToken.absoluteString, - "https://rum-http-intake.logs.datadoghq.eu/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.eu).rum?.uploadURL.absoluteString, + "https://rum-http-intake.logs.datadoghq.eu/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: DeprecatedEndpoints.gov).rum?.uploadURLWithClientToken.absoluteString, - "https://rum.browser-intake-ddog-gov.com/v1/input/" + clientToken + try configuration(datadogEndpoint: DeprecatedEndpoints.gov).rum?.uploadURL.absoluteString, + "https://rum.browser-intake-ddog-gov.com/api/v2/rum" ) XCTAssertEqual( - try configuration(datadogEndpoint: nil).logging?.uploadURLWithClientToken.absoluteString, - randomLogsEndpoint.url + clientToken, + try configuration(datadogEndpoint: nil).logging?.uploadURL.absoluteString, + randomLogsEndpoint.url, "When `DatadogEndpoint` is not set, it should default to `LogsEndpoint` value." ) XCTAssertEqual( - try configuration(datadogEndpoint: nil).tracing?.uploadURLWithClientToken.absoluteString, - randomTracesEndpoint.url + clientToken, + try configuration(datadogEndpoint: nil).tracing?.uploadURL.absoluteString, + randomTracesEndpoint.url, "When `DatadogEndpoint` is not set, it should default to `TracesEndpoint` value." ) XCTAssertEqual( - try configuration(datadogEndpoint: nil).rum?.uploadURLWithClientToken.absoluteString, - randomRUMEndpoint.url + clientToken, + try configuration(datadogEndpoint: nil).rum?.uploadURL.absoluteString, + randomRUMEndpoint.url, "When `DatadogEndpoint` is not set, it should default to `RUMEndpoint` value." ) } @@ -253,19 +261,17 @@ class FeaturesConfigurationTests: XCTestCase { } func testCustomLogsEndpoint() throws { - let clientToken: String = .mockRandom(among: "abcdef") let randomDatadogEndpoint: Datadog.Configuration.DatadogEndpoint = .mockRandom() let randomCustomEndpoint: URL = .mockRandom() let configuration = try createConfiguration( - clientToken: clientToken, datadogEndpoint: randomDatadogEndpoint, customLogsEndpoint: randomCustomEndpoint ) XCTAssertEqual( - configuration.logging?.uploadURLWithClientToken, - randomCustomEndpoint.appendingPathComponent(clientToken), + configuration.logging?.uploadURL, + randomCustomEndpoint, "When custom endpoint is set it should override `DatadogEndpoint`" ) } @@ -280,19 +286,17 @@ class FeaturesConfigurationTests: XCTestCase { } func testCustomTracesEndpoint() throws { - let clientToken: String = .mockRandom(among: "abcdef") let randomDatadogEndpoint: Datadog.Configuration.DatadogEndpoint = .mockRandom() let randomCustomEndpoint: URL = .mockRandom() let configuration = try createConfiguration( - clientToken: clientToken, datadogEndpoint: randomDatadogEndpoint, customTracesEndpoint: randomCustomEndpoint ) XCTAssertEqual( - configuration.tracing?.uploadURLWithClientToken, - randomCustomEndpoint.appendingPathComponent(clientToken), + configuration.tracing?.uploadURL, + randomCustomEndpoint, "When custom endpoint is set it should override `DatadogEndpoint`" ) } @@ -307,19 +311,17 @@ class FeaturesConfigurationTests: XCTestCase { } func testCustomRUMEndpoint() throws { - let clientToken: String = .mockRandom(among: "abcdef") let randomDatadogEndpoint: Datadog.Configuration.DatadogEndpoint = .mockRandom() let randomCustomEndpoint: URL = .mockRandom() let configuration = try createConfiguration( - clientToken: clientToken, datadogEndpoint: randomDatadogEndpoint, customRUMEndpoint: randomCustomEndpoint ) XCTAssertEqual( - configuration.rum?.uploadURLWithClientToken, - randomCustomEndpoint.appendingPathComponent(clientToken), + configuration.rum?.uploadURL, + randomCustomEndpoint, "When custom endpoint is set it should override `DatadogEndpoint`" ) } @@ -578,13 +580,16 @@ class FeaturesConfigurationTests: XCTestCase { func testWhenInternalMonitoringClientTokenIsSet_thenInternalMonitoringConfigurationIsEnabled() throws { // When let internalMonitoringClientToken: String = .mockRandom(among: "abcdef") + let featuresClientToken: String = .mockRandom(among: "ghijkl") let featuresConfiguration = try FeaturesConfiguration( - configuration: .mockWith(internalMonitoringClientToken: internalMonitoringClientToken), - appContext: .mockWith( - bundleIdentifier: "com.bundle.identifier", - bundleVersion: "1.2.3", - bundleName: "AppName" - ) + configuration: .mockWith( + clientToken: featuresClientToken, + loggingEnabled: true, + tracingEnabled: true, + rumEnabled: true, + internalMonitoringClientToken: internalMonitoringClientToken + ), + appContext: .mockAny() ) // Then @@ -593,9 +598,13 @@ class FeaturesConfigurationTests: XCTestCase { XCTAssertEqual(configuration.sdkServiceName, "dd-sdk-ios", "Internal monitoring data should be available under \"service:dd-sdk-ios\"") XCTAssertEqual(configuration.sdkEnvironment, "prod", "Internal monitoring data should be available under \"env:prod\"") XCTAssertEqual( - configuration.logsUploadURLWithClientToken.absoluteString, - "https://logs.browser-intake-datadoghq.com/v1/input/" + internalMonitoringClientToken + configuration.logsUploadURL.absoluteString, + "https://logs.browser-intake-datadoghq.com/api/v2/logs" ) + XCTAssertEqual(configuration.clientToken, internalMonitoringClientToken, "Internal Monitoring must use monitoring token") + XCTAssertEqual(featuresConfiguration.logging!.clientToken, featuresClientToken, "Logging must use feature token") + XCTAssertEqual(featuresConfiguration.tracing!.clientToken, featuresClientToken, "Tracing must use feature token") + XCTAssertEqual(featuresConfiguration.rum!.clientToken, featuresClientToken, "RUM must use feature token") } // MARK: - Invalid Configurations diff --git a/Tests/DatadogTests/Datadog/Core/Upload/DataUploadStatusTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadStatusTests.swift new file mode 100644 index 0000000000..6051415da7 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadStatusTests.swift @@ -0,0 +1,162 @@ +/* + * 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 DataUploadStatusTests: XCTestCase { + // MARK: - Test `.needsRetry` + + private let statusCodesExpectingNoRetry = [ + 202, // ACCEPTED + 400, // BAD REQUEST + 401, // UNAUTHORIZED + 403, // FORBIDDEN + 413, // PAYLOAD TOO LARGE + ] + + private let statusCodesExpectingRetry = [ + 408, // REQUEST TIMEOUT + 429, // TOO MANY REQUESTS + 500, // INTERNAL SERVER ERROR + 503, // SERVICE UNAVAILABLE + ] + + private lazy var expectedStatusCodes = statusCodesExpectingNoRetry + statusCodesExpectingRetry + + func testWhenUploadFinishesWithResponse_andStatusCodeNeedsNoRetry_itSetsNeedsRetryFlagToFalse() { + statusCodesExpectingNoRetry.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") + } + } + + func testWhenUploadFinishesWithResponse_andStatusCodeNeedsRetry_itSetsNeedsRetryFlagToTrue() { + statusCodesExpectingRetry.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + XCTAssertTrue(status.needsRetry, "Upload should be retried for status code \(statusCode)") + } + } + + func testWhenUploadFinishesWithResponse_andStatusCodeIsUnexpected_itSetsNeedsRetryFlagToFalse() { + let allStatusCodes = Set((100...599)) + let unexpectedStatusCodes = allStatusCodes.subtracting(Set(expectedStatusCodes)) + + unexpectedStatusCodes.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") + } + } + + func testWhenUploadFinishesWithError_itSetsNeedsRetryFlagToTrue() { + let status = DataUploadStatus(networkError: ErrorMock()) + XCTAssertTrue(status.needsRetry, "Upload should be retried if it finished with error") + } + + // MARK: - Test `.userDebugDescription` + + func testWhenUploadFinishesWithResponse_andRequestIDIsAvailable_itCreatesUserDebugDescription() { + expectedStatusCodes.forEach { statusCode in + let requestID: String = .mockRandom(among: .alphanumerics) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID) + XCTAssertTrue( + status.userDebugDescription.matches( + regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \(requestID)\\]" + ), + "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and request id '\(requestID)'" + ) + } + } + + func testWhenUploadFinishesWithResponse_andRequestIDIsNotAvailable_itCreatesUserDebugDescription() { + expectedStatusCodes.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + XCTAssertTrue( + status.userDebugDescription.matches( + regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \\(\\?\\?\\?\\)\\]" + ), + "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and no request id" + ) + } + } + + func testWhenUploadFinishesWithError_itCreatesUserDebugDescription() { + let randomErrorDescription: String = .mockRandom() + let status = DataUploadStatus(networkError: ErrorMock(randomErrorDescription)) + XCTAssertEqual(status.userDebugDescription, "[error: \(randomErrorDescription)]") + } + + // MARK: - Test `.userErrorMessage` + + func testWhenUploadFinishesWithResponse_andStatusCodeIs401_itCreatesClientTokenErrorMessage() { + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 401), ddRequestID: nil) + XCTAssertEqual(status.userErrorMessage, "⚠️ The client token you provided seems to be invalid.") + } + + func testWhenUploadFinishesWithResponse_andStatusCodeIsDifferentThan401_itDoesNotCreateAnyUserErrorMessage() { + let statusCodes = Set((100...599)).subtracting([401]) + statusCodes.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + XCTAssertNil(status.userErrorMessage) + } + } + + func testWhenUploadFinishesWithError_itDoesNotCreateAnyUserErrorMessage() { + let status = DataUploadStatus(networkError: ErrorMock(.mockRandom())) + XCTAssertNil(status.userErrorMessage) + } + + // MARK: - Test `.internalMonitoringError` + + private let alertingStatusCodes = [ + 400, // BAD REQUEST + 413, // PAYLOAD TOO LARGE + 408, // REQUEST TIMEOUT + 429, // TOO MANY REQUESTS + ] + + func testWhenUploadFinishesWithResponse_andStatusCodeMeansSDKIssue_itCreatesInternalMonitoringError() throws { + try alertingStatusCodes.forEach { statusCode in + let requestID: String = .mockRandom() + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID) + let error = try XCTUnwrap(status.internalMonitoringError, "Internal Monitoring error should be created for status code \(statusCode)") + XCTAssertEqual(error.message, "Data upload finished with status code: \(statusCode)") + XCTAssertEqual(error.attributes?["dd_request_id"], requestID) + } + } + + func testWhenUploadFinishesWithResponse_andStatusCodeMeansClientIssue_itDoesNotCreateInternalMonitoringError() { + let clientIssueStatusCodes = Set(expectedStatusCodes).subtracting(Set(alertingStatusCodes)) + clientIssueStatusCodes.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + XCTAssertNil(status.internalMonitoringError, "Internal Monitoring error should not be created for status code \(statusCode)") + } + } + + func testWhenUploadFinishesWithResponse_andUnexpectedStatusCodeMeansClientIssue_itDoesNotCreateInternalMonitoringError() { + let unexpectedStatusCodes = Set((100...599)).subtracting(Set(expectedStatusCodes)) + unexpectedStatusCodes.forEach { statusCode in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + XCTAssertNil(status.internalMonitoringError) + } + } + + func testWhenUploadFinishesWithError_andErrorCodeMeansSDKIssue_itCreatesInternalMonitoringError() throws { + let alertingNSURLErrorCode = NSURLErrorBadURL + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: alertingNSURLErrorCode, userInfo: nil)) + + let error = try XCTUnwrap(status.internalMonitoringError, "Internal Monitoring error should be created for NSURLError code: \(alertingNSURLErrorCode)") + XCTAssertEqual(error.message, "Data upload finished with error") + let nsError = try XCTUnwrap(error.error) as NSError + XCTAssertEqual(nsError.code, alertingNSURLErrorCode) + } + + func testWhenUploadFinishesWithError_andErrorCodeMeansExternalFactors_itDoesNotCreateInternalMonitoringError() { + let notAlertingNSURLErrorCode = NSURLErrorNetworkConnectionLost + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: notAlertingNSURLErrorCode, userInfo: nil)) + XCTAssertNil(status.internalMonitoringError, "Internal Monitoring error should be created for NSURLError code: \(notAlertingNSURLErrorCode)") + } +} diff --git a/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 19df1f28ca..26f96cc3e9 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -35,12 +35,13 @@ class DataUploadWorkerTests: XCTestCase { super.tearDown() } + // MARK: - Data Uploads + func testItUploadsAllData() { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) // Given @@ -69,6 +70,62 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try temporaryDirectory.files().count, 0) } + func testGivenDataToUpload_whenUploadFinishesAndDoesNotNeedToBeRetried_thenDataIsDeleted() { + let startUploadExpectation = self.expectation(description: "Upload has started") + + var mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: false)) + mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + + // Given + writer.write(value: ["key": "value"]) + XCTAssertEqual(try temporaryDirectory.files().count, 1) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny() + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try temporaryDirectory.files().count, 0, "When upload finishes with `needsRetry: false`, data should be deleted") + } + + func testGivenDataToUpload_whenUploadFinishesAndNeedsToBeRetried_thenDataIsPreserved() { + let startUploadExpectation = self.expectation(description: "Upload has started") + + var mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: true)) + mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + + // Given + writer.write(value: ["key": "value"]) + XCTAssertEqual(try temporaryDirectory.files().count, 1) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny() + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try temporaryDirectory.files().count, 1, "When upload finishes with `needsRetry: true`, data should be preserved") + } + + // MARK: - Upload Interval Changes + func testWhenThereIsNoBatch_thenIntervalIncreases() { let delayChangeExpectation = expectation(description: "Upload delay is increased") let mockDelay = MockDelay { command in @@ -84,9 +141,8 @@ class DataUploadWorkerTests: XCTestCase { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) let worker = DataUploadWorker( queue: uploaderQueue, @@ -118,9 +174,8 @@ class DataUploadWorkerTests: XCTestCase { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 500))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) let worker = DataUploadWorker( queue: uploaderQueue, @@ -152,9 +207,8 @@ class DataUploadWorkerTests: XCTestCase { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) let worker = DataUploadWorker( queue: uploaderQueue, @@ -171,13 +225,78 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() } + // MARK: - Notifying Upload Progress + + func testWhenDataIsBeingUploaded_itPrintsUploadProgressInformationAndSendsErrorsThroughInternalMonitoring() { + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + + let mockUserLoggerOutput = LogOutputMock() + userLogger = .mockWith(logOutput: mockUserLoggerOutput) + + let mockSDKLoggerOutput = LogOutputMock() + + // Given + writer.write(value: ["key": "value"]) + + let randomUploadStatus: DataUploadStatus = .mockRandom() + let randomFeatureName: String = .mockRandom() + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + var mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) + mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: randomFeatureName, + internalMonitor: InternalMonitor( + sdkLogger: .mockWith(logOutput: mockSDKLoggerOutput) + ) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + let expectedSummary = randomUploadStatus.needsRetry ? "not delivered, will be retransmitted" : "accepted, won't be retransmitted" + XCTAssertEqual(mockUserLoggerOutput.allRecordedLogs.count, 3) + XCTAssertEqual( + mockUserLoggerOutput.allRecordedLogs[0].message, + "⏳ (\(randomFeatureName)) Uploading batch...", + "Batch start information should be printed to `userLogger`" + ) + XCTAssertEqual( + mockUserLoggerOutput.allRecordedLogs[1].message, + " → (\(randomFeatureName)) \(expectedSummary): \(randomUploadStatus.userDebugDescription)", + "Batch completion information should be printed to `userLogger`" + ) + XCTAssertEqual( + mockUserLoggerOutput.allRecordedLogs[2].message, + randomUploadStatus.userErrorMessage, + "An error should be printed to `userLogger`" + ) + + XCTAssertEqual(mockSDKLoggerOutput.allRecordedLogs.count, 1) + XCTAssertEqual( + mockSDKLoggerOutput.allRecordedLogs[0].message, + randomUploadStatus.internalMonitoringError?.message, + "An error should be send to `sdkLogger` for internal monitoring" + ) + } + + // MARK: - Tearing Down + func testWhenCancelled_itPerformsNoMoreUploads() { // Given let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) let worker = DataUploadWorker( queue: uploaderQueue, @@ -200,9 +319,8 @@ class DataUploadWorkerTests: XCTestCase { func testItFlushesAllData() { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) let worker = DataUploadWorker( queue: uploaderQueue, @@ -231,155 +349,6 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() } - - func testGivenDataToUpload_whenUploadFinishesWithSuccessStatusCode_thenDataIsDeleted() { - let statusCode = (200...299).randomElement()! - - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: statusCode))) - let dataUploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - - // Given - writer.write(value: ["key": "value"]) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - // When - let worker = DataUploadWorker( - queue: uploaderQueue, - fileReader: reader, - dataUploader: dataUploader, - uploadConditions: DataUploadConditions.alwaysUpload(), - delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), - featureName: .mockAny() - ) - _ = server.waitAndReturnRequests(count: 1) - - // Then - worker.cancelSynchronously() - XCTAssertEqual(try temporaryDirectory.files().count, 0, "When status code \(statusCode) is received, data should be deleted") - } - - func testGivenDataToUpload_whenUploadFinishesWithRedirectStatusCode_thenDataIsDeleted() { - let statusCode = (300...399).randomElement()! - - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: statusCode))) - let dataUploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - - // Given - writer.write(value: ["key": "value"]) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - // When - let worker = DataUploadWorker( - queue: uploaderQueue, - fileReader: reader, - dataUploader: dataUploader, - uploadConditions: DataUploadConditions.alwaysUpload(), - delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), - featureName: .mockAny() - ) - _ = server.waitAndReturnRequests(count: 1) - - // Then - worker.cancelSynchronously() - XCTAssertEqual(try temporaryDirectory.files().count, 0, "When status code \(statusCode) is received, data should be deleted") - } - - func testGivenDataToUpload_whenUploadFinishesWithClientErrorStatusCode_thenDataIsDeleted() { - let statusCode = (400...499).randomElement()! - - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: statusCode))) - let dataUploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - - // Given - writer.write(value: ["key": "value"]) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - // When - let worker = DataUploadWorker( - queue: uploaderQueue, - fileReader: reader, - dataUploader: dataUploader, - uploadConditions: DataUploadConditions.alwaysUpload(), - delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), - featureName: .mockAny() - ) - _ = server.waitAndReturnRequests(count: 1) - - // Then - worker.cancelSynchronously() - XCTAssertEqual(try temporaryDirectory.files().count, 0, "When status code \(statusCode) is received, data should be deleted") - } - - func testGivenDataToUpload_whenUploadFinishesWithClientTokenErrorStatusCode_thenDataIsDeleted() { - let statusCode = 403 - - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: statusCode))) - let dataUploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - - // Given - writer.write(value: ["key": "value"]) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - // When - let worker = DataUploadWorker( - queue: uploaderQueue, - fileReader: reader, - dataUploader: dataUploader, - uploadConditions: DataUploadConditions.alwaysUpload(), - delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), - featureName: .mockAny() - ) - _ = server.waitAndReturnRequests(count: 1) - - // Then - worker.cancelSynchronously() - XCTAssertEqual(try temporaryDirectory.files().count, 0, "When status code \(statusCode) is received, data should be deleted") - } - - func testGivenDataToUpload_whenUploadFinishesWithServerErrorStatusCode_thenDataIsPreserved() { - let statusCode = (500...599).randomElement()! - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: statusCode))) - let dataUploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - - // Given - writer.write(value: ["key": "value"]) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - // When - let worker = DataUploadWorker( - queue: uploaderQueue, - fileReader: reader, - dataUploader: dataUploader, - uploadConditions: DataUploadConditions.alwaysUpload(), - delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), - featureName: .mockAny() - ) - _ = server.waitAndReturnRequests(count: 1) - - // Then - worker.cancelSynchronously() - XCTAssertEqual(try temporaryDirectory.files().count, 1, "When status code \(statusCode) is received, data should be preserved") - } } struct MockDelay: Delay { diff --git a/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift index 885702a295..aa6e285ca4 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift @@ -7,135 +7,51 @@ import XCTest @testable import Datadog -class DataUploadURLProviderTests: XCTestCase { - func testDDSourceQueryItem() { - let item: UploadURLProvider.QueryItemProvider = .ddsource(source: "abc") - - XCTAssertEqual(item.value().name, "ddsource") - XCTAssertEqual(item.value().value, "abc") - } - - func testItBuildsValidURLUsingNoQueryItems() throws { - let urlProvider = UploadURLProvider( - urlWithClientToken: URL(string: "https://api.example.com/v1/endpoint/abc")!, - queryItemProviders: [] - ) - - XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc")) - } - - func testItBuildsValidURLUsingAllQueryItems() throws { - let urlProvider = UploadURLProvider( - urlWithClientToken: URL(string: "https://api.example.com/v1/endpoint/abc")!, - queryItemProviders: [.ddsource(source: "abc"), .ddtags(tags: ["abc:def"])] - ) - - XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?ddsource=abc&ddtags=abc:def")) - XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?ddsource=abc&ddtags=abc:def")) - } - - func testItEscapesWhitespacesInQueryItems() throws { - let urlProvider = UploadURLProvider( - urlWithClientToken: URL(string: "https://api.example.com/v1/endpoint/abc")!, - queryItemProviders: [.ddtags(tags: ["some string with whitespace"])] - ) - - XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?ddtags=some%20string%20with%20whitespace")) - } -} +extension DataUploadStatus: EquatableInTests {} class DataUploaderTests: XCTestCase { - // MARK: - Upload Status - - func testWhenDataIsSentWith200Code_itReturnsDataUploadStatus_success() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - let status = uploader.upload(data: .mockAny()) - - XCTAssertEqual(status, .success) - server.waitFor(requestsCompletion: 1) - } - - func testWhenDataIsSentWith300Code_itReturnsDataUploadStatus_redirection() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 300))) + func testWhenUploadCompletesWithSuccess_itReturnsExpectedUploadStatus() { + // Given + let randomResponse: HTTPURLResponse = .mockResponseWith(statusCode: (100...599).randomElement()!) + let randomRequestIDOrNil: String? = Bool.random() ? .mockRandom() : nil + let requestIDHeaderOrNil: RequestBuilder.HTTPHeader? = randomRequestIDOrNil.flatMap { randomRequestID in + .init(field: RequestBuilder.HTTPHeader.ddRequestIDHeaderField, value: .constant(randomRequestID)) + } + + let server = ServerMock(delivery: .success(response: randomResponse)) let uploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockWith(headers: requestIDHeaderOrNil.map { [$0] } ?? []) ) - let status = uploader.upload(data: .mockAny()) - XCTAssertEqual(status, .redirection) - server.waitFor(requestsCompletion: 1) - } + // When + let uploadStatus = uploader.upload(data: .mockAny()) - func testWhenDataIsSentWith400Code_itReturnsDataUploadStatus_clientError() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 400))) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - let status = uploader.upload(data: .mockAny()) + // Then + let expectedUploadStatus = DataUploadStatus(httpResponse: randomResponse, ddRequestID: randomRequestIDOrNil) - XCTAssertEqual(status, .clientError) + XCTAssertEqual(uploadStatus, expectedUploadStatus) server.waitFor(requestsCompletion: 1) } - func testWhenDataIsSentWith403Code_itReturnsDataUploadStatus_clientTokenError() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 403))) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - let status = uploader.upload(data: .mockAny()) - - XCTAssertEqual(status, .clientTokenError) - server.waitFor(requestsCompletion: 1) - } + func testWhenUploadCompletesWithFailure_itReturnsExpectedUploadStatus() { + // Given + let randomErrorDescription: String = .mockRandom() + let randomError = NSError(domain: .mockRandom(), code: .mockRandom(), userInfo: [NSLocalizedDescriptionKey: randomErrorDescription]) - func testWhenDataIsSentWith500Code_itReturnsDataUploadStatus_serverError() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 500))) + let server = ServerMock(delivery: .failure(error: randomError)) let uploader = DataUploader( - urlProvider: .mockAny(), httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() + requestBuilder: .mockAny() ) - let status = uploader.upload(data: .mockAny()) - XCTAssertEqual(status, .serverError) - server.waitFor(requestsCompletion: 1) - } - - func testWhenDataIsNotSentDueToNetworkError_itReturnsDataUploadStatus_networkError() { - let mockError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "network error"]) - let server = ServerMock(delivery: .failure(error: mockError)) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - let status = uploader.upload(data: .mockAny()) + // When + let uploadStatus = uploader.upload(data: .mockAny()) - XCTAssertEqual(status, .networkError) - server.waitFor(requestsCompletion: 1) - } - - func testWhenDataIsNotSentDueToUnknownStatusCode_itReturnsDataUploadStatus_unknown() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: -1))) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.getInterceptedURLSession()), - httpHeaders: .mockAny() - ) - let status = uploader.upload(data: .mockAny()) + // Then + let expectedUploadStatus = DataUploadStatus(networkError: randomError) - XCTAssertEqual(status, .unknown) + XCTAssertEqual(uploadStatus, expectedUploadStatus) server.waitFor(requestsCompletion: 1) } } diff --git a/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift deleted file mode 100644 index 17873d3e32..0000000000 --- a/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class HTTPHeadersTests: XCTestCase { - func testContentTypeHeader() { - let applicationJSON = HTTPHeaders.HTTPHeader.contentTypeHeader(contentType: .applicationJSON) - XCTAssertEqual(applicationJSON.field, "Content-Type") - XCTAssertEqual(applicationJSON.value, "application/json") - - let plainText = HTTPHeaders.HTTPHeader.contentTypeHeader(contentType: .textPlainUTF8) - XCTAssertEqual(plainText.field, "Content-Type") - XCTAssertEqual(plainText.value, "text/plain;charset=UTF-8") - } - - func testUserAgentHeader() { - let userAgent = HTTPHeaders.HTTPHeader.userAgentHeader( - appName: "FoobarApp", - appVersion: "1.2.3", - device: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") - ) - XCTAssertEqual(userAgent.field, "User-Agent") - XCTAssertEqual(userAgent.value, "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)") - } - - func testComposingHeaders() { - let headers = HTTPHeaders( - headers: [ - .contentTypeHeader(contentType: .applicationJSON), - .userAgentHeader( - appName: "FoobarApp", - appVersion: "1.2.3", - device: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") - ) - ] - ) - - XCTAssertEqual( - headers.all, - [ - "Content-Type": "application/json", - "User-Agent": "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)" - ] - ) - } -} diff --git a/Tests/DatadogTests/Datadog/Core/Upload/RequestBuilderTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/RequestBuilderTests.swift new file mode 100644 index 0000000000..cb5499a002 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Upload/RequestBuilderTests.swift @@ -0,0 +1,145 @@ +/* + * 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 RequestBuilderTests: XCTestCase { + // MARK: - Request URL + + func testBuildingRequestWithURLAndQueryItems() throws { + let randomURL: URL = .mockRandom() + let builder = RequestBuilder( + url: randomURL, + queryItems: [.ddsource(source: "abc"), .ddtags(tags: ["abc:def"])], + headers: .mockRandom() + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.url?.absoluteString, "\(randomURL.absoluteString)?ddsource=abc&ddtags=abc:def") + } + + func testWhenBuildingRequestWithURLAndQueryItems_itEscapesWhitespacesInQuery() throws { + let randomURL: URL = .mockRandom() + let builder = RequestBuilder( + url: randomURL, + queryItems: [.ddsource(source: "source with whitespace"), .ddtags(tags: ["tag with whitespace"])], + headers: .mockRandom() + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.url?.absoluteString, "\(randomURL.absoluteString)?ddsource=source%20with%20whitespace&ddtags=tag%20with%20whitespace") + } + + // MARK: - Request Headers + + func testBuildingRequestWithContentTypeHeader() { + var builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.contentTypeHeader(contentType: .textPlainUTF8)]) + var request = builder.uploadRequest(with: .mockAny()) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "text/plain;charset=UTF-8") + + builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.contentTypeHeader(contentType: .applicationJSON)]) + request = builder.uploadRequest(with: .mockAny()) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") + } + + func testBuildingRequestWithUserAgentHeader() { + let builder = RequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [ + .userAgentHeader( + appName: "FoobarApp", + appVersion: "1.2.3", + device: .mockWith( + model: "iPhone", + osName: "iOS", + osVersion: "13.3.1" + ) + ) + ] + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)") + } + + func testBuildingRequestWithDDAPIKeyHeader() { + let randomClientToken: String = .mockRandom() + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddAPIKeyHeader(clientToken: randomClientToken)]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + } + + func testBuildingRequestWithDDEVPOriginHeader() { + let randomSource: String = .mockRandom() + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddEVPOriginHeader(source: randomSource)]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource) + } + + func testBuildingRequestWithDDEVPOriginVersionHeader() { + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddEVPOriginVersionHeader()]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], sdkVersion) + } + + func testBuildingRequestWithDDRequestIDHeader() throws { + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddRequestIDHeader()]) + + let request1 = builder.uploadRequest(with: .mockRandom()) + let request2 = builder.uploadRequest(with: .mockRandom()) + let request3 = builder.uploadRequest(with: .mockRandom()) + + let requestID1 = try XCTUnwrap(request1.allHTTPHeaderFields?["DD-REQUEST-ID"]) + let requestID2 = try XCTUnwrap(request2.allHTTPHeaderFields?["DD-REQUEST-ID"]) + let requestID3 = try XCTUnwrap(request3.allHTTPHeaderFields?["DD-REQUEST-ID"]) + + let allIDs = Set([requestID1, requestID2, requestID3]) + XCTAssertEqual(allIDs.count, 3, "Each `DD-REQUEST-ID` must produce unique ID") + allIDs.forEach { id in + XCTAssertTrue(id.matches(regex: .uuidRegex), "Each `DD-REQUEST-ID` must be an UUID string") + } + } + + func testBuildingRequestWithMultipleHeaders() { + let builder = RequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [ + .contentTypeHeader(contentType: .textPlainUTF8), + .userAgentHeader(appName: .mockAny(), appVersion: .mockAny(), device: .mockAny()), + .ddAPIKeyHeader(clientToken: .mockAny()), + .ddEVPOriginHeader(source: .mockAny()), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader(), + ] + ) + + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Type"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["User-Agent"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-API-KEY"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-REQUEST-ID"]) + XCTAssertEqual(request.allHTTPHeaderFields?.count, 6) + } + + // MARK: - Request Method + + func testItUsesPOSTMethodForProducedReqest() { + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.httpMethod, "POST") + } + + // MARK: - Request Data + + func testItSetsDataAsHTTPBodyInProducedRequest() { + let randomData: Data = .mockRandom() + let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + let request = builder.uploadRequest(with: randomData) + XCTAssertEqual(request.httpBody, randomData) + } +} diff --git a/Tests/DatadogTests/Datadog/InternalMonitoring/InternalMonitoringFeatureTests.swift b/Tests/DatadogTests/Datadog/InternalMonitoring/InternalMonitoringFeatureTests.swift index 37eb74975f..bc1f9ffab9 100644 --- a/Tests/DatadogTests/Datadog/InternalMonitoring/InternalMonitoringFeatureTests.swift +++ b/Tests/DatadogTests/Datadog/InternalMonitoring/InternalMonitoringFeatureTests.swift @@ -25,30 +25,56 @@ class InternalMonitoringFeatureTests: XCTestCase { // MARK: - HTTP Message func testItUsesExpectedHTTPMessage() throws { + let randomApplicationName: String = .mockRandom() + let randomApplicationVersion: String = .mockRandom() + let randomSource: String = .mockRandom(among: .alphanumerics) + let randomUploadURL: URL = .mockRandom() + let randomClientToken: String = .mockRandom() + let randomDeviceModel: String = .mockRandom() + let randomDeviceOSName: String = .mockRandom() + let randomDeviceOSVersion: String = .mockRandom() + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + + // Given InternalMonitoringFeature.instance = .mockWith( logDirectories: temporaryFeatureDirectories, configuration: .mockWith( common: .mockWith( - applicationName: "FoobarApp", - applicationVersion: "2.1.0", - source: "abc" - ) + applicationName: randomApplicationName, + applicationVersion: randomApplicationVersion, + source: randomSource + ), + logsUploadURL: randomUploadURL, + clientToken: randomClientToken ), dependencies: .mockWith( - mobileDevice: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") + mobileDevice: .mockWith(model: randomDeviceModel, osName: randomDeviceOSName, osVersion: randomDeviceOSVersion) ) ) defer { InternalMonitoringFeature.instance?.deinitialize() } + // When let sdkLogger = try XCTUnwrap(InternalMonitoringFeature.instance?.monitor.sdkLogger) - sdkLogger.debug("message") + sdkLogger.debug(.mockAny()) + // Then let request = server.waitAndReturnRequests(count: 1)[0] + let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(request.url?.query, "ddsource=abc") - XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "FoobarApp/2.1.0 CFNetwork (iPhone; iOS/13.3.1)") + XCTAssertTrue(requestURL.absoluteString.starts(with: randomUploadURL.absoluteString + "?")) + XCTAssertEqual(requestURL.query, "ddsource=\(randomSource)") + XCTAssertEqual( + request.allHTTPHeaderFields?["User-Agent"], + """ + \(randomApplicationName)/\(randomApplicationVersion) CFNetwork (\(randomDeviceModel); \(randomDeviceOSName)/\(randomDeviceOSVersion)) + """ + ) XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], sdkVersion) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-REQUEST-ID"]?.matches(regex: .uuidRegex), true) } // MARK: - Sending SDK Logs diff --git a/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift b/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift index ea787c432a..46957f18c0 100644 --- a/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift +++ b/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift @@ -25,30 +25,56 @@ class LoggingFeatureTests: XCTestCase { // MARK: - HTTP Message func testItUsesExpectedHTTPMessage() throws { + let randomApplicationName: String = .mockRandom() + let randomApplicationVersion: String = .mockRandom() + let randomSource: String = .mockRandom(among: .alphanumerics) + let randomUploadURL: URL = .mockRandom() + let randomClientToken: String = .mockRandom() + let randomDeviceModel: String = .mockRandom() + let randomDeviceOSName: String = .mockRandom() + let randomDeviceOSVersion: String = .mockRandom() + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + + // Given LoggingFeature.instance = .mockWith( directories: temporaryFeatureDirectories, configuration: .mockWith( common: .mockWith( - applicationName: "FoobarApp", - applicationVersion: "2.1.0", - source: "abc" - ) + applicationName: randomApplicationName, + applicationVersion: randomApplicationVersion, + source: randomSource + ), + uploadURL: randomUploadURL, + clientToken: randomClientToken ), dependencies: .mockWith( - mobileDevice: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") + mobileDevice: .mockWith(model: randomDeviceModel, osName: randomDeviceOSName, osVersion: randomDeviceOSVersion) ) ) defer { LoggingFeature.instance?.deinitialize() } + // When let logger = Logger.builder.build() - logger.debug("message") + logger.debug(.mockAny()) + // Then let request = server.waitAndReturnRequests(count: 1)[0] + let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(request.url?.query, "ddsource=abc") - XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "FoobarApp/2.1.0 CFNetwork (iPhone; iOS/13.3.1)") + XCTAssertTrue(requestURL.absoluteString.starts(with: randomUploadURL.absoluteString + "?")) + XCTAssertEqual(requestURL.query, "ddsource=\(randomSource)") + XCTAssertEqual( + request.allHTTPHeaderFields?["User-Agent"], + """ + \(randomApplicationName)/\(randomApplicationVersion) CFNetwork (\(randomDeviceModel); \(randomDeviceOSName)/\(randomDeviceOSVersion)) + """ + ) XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], sdkVersion) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-REQUEST-ID"]?.matches(regex: .uuidRegex), true) } // MARK: - HTTP Payload diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index f9ba18e7fa..3fbce215db 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -190,9 +190,10 @@ extension FeaturesConfiguration.Logging { static func mockWith( common: FeaturesConfiguration.Common = .mockAny(), - uploadURLWithClientToken: URL = .mockAny() + uploadURL: URL = .mockAny(), + clientToken: String = .mockAny() ) -> Self { - return .init(common: common, uploadURLWithClientToken: uploadURLWithClientToken) + return .init(common: common, uploadURL: uploadURL, clientToken: clientToken) } } @@ -201,12 +202,14 @@ extension FeaturesConfiguration.Tracing { static func mockWith( common: FeaturesConfiguration.Common = .mockAny(), - uploadURLWithClientToken: URL = .mockAny(), - spanEventMapper: SpanEventMapper? = nil + uploadURL: URL = .mockAny(), + spanEventMapper: SpanEventMapper? = nil, + clientToken: String = .mockAny() ) -> Self { return .init( common: common, - uploadURLWithClientToken: uploadURLWithClientToken, + uploadURL: uploadURL, + clientToken: clientToken, spanEventMapper: spanEventMapper ) } @@ -217,7 +220,8 @@ extension FeaturesConfiguration.RUM { static func mockWith( common: FeaturesConfiguration.Common = .mockAny(), - uploadURLWithClientToken: URL = .mockAny(), + uploadURL: URL = .mockAny(), + clientToken: String = .mockAny(), applicationID: String = .mockAny(), sessionSamplingRate: Float = 100.0, viewEventMapper: RUMViewEventMapper? = nil, @@ -229,7 +233,8 @@ extension FeaturesConfiguration.RUM { ) -> Self { return .init( common: common, - uploadURLWithClientToken: uploadURLWithClientToken, + uploadURL: uploadURL, + clientToken: clientToken, applicationID: applicationID, sessionSamplingRate: sessionSamplingRate, viewEventMapper: viewEventMapper, @@ -285,13 +290,15 @@ extension FeaturesConfiguration.InternalMonitoring { common: FeaturesConfiguration.Common = .mockAny(), sdkServiceName: String = .mockAny(), sdkEnvironment: String = .mockAny(), - logsUploadURLWithClientToken: URL = .mockAny() + logsUploadURL: URL = .mockAny(), + clientToken: String = .mockAny() ) -> Self { return .init( common: common, sdkServiceName: sdkServiceName, sdkEnvironment: sdkEnvironment, - logsUploadURLWithClientToken: logsUploadURLWithClientToken + logsUploadURL: logsUploadURL, + clientToken: clientToken ) } } @@ -522,11 +529,6 @@ class NoOpFileReader: SyncReader { func markAllFilesAsReadable() {} } -class NoOpDataUploadWorker: DataUploadWorkerType { - func flushSynchronously() {} - func cancelSynchronously() {} -} - extension DataFormat { static func mockAny() -> DataFormat { return mockWith() @@ -640,12 +642,49 @@ extension UserInfoProvider { } } -extension UploadURLProvider { - static func mockAny() -> UploadURLProvider { - return UploadURLProvider( - urlWithClientToken: URL(string: "https://app.example.com/v2/api?abc-def-ghi")!, - queryItemProviders: [] - ) +extension RequestBuilder.QueryItem: RandomMockable, AnyMockable { + static func mockRandom() -> RequestBuilder.QueryItem { + let all: [RequestBuilder.QueryItem] = [ + .ddsource(source: .mockRandom()), + .ddtags(tags: .mockRandom()), + ] + return all.randomElement()! + } + + static func mockAny() -> RequestBuilder.QueryItem { + return .ddsource(source: .mockRandom(among: .alphanumerics)) + } +} + +extension RequestBuilder.HTTPHeader: RandomMockable, AnyMockable { + static func mockRandom() -> RequestBuilder.HTTPHeader { + let all: [RequestBuilder.HTTPHeader] = [ + .contentTypeHeader(contentType: Bool.random() ? .applicationJSON : .textPlainUTF8), + .userAgentHeader(appName: .mockRandom(among: .alphanumerics), appVersion: .alphanumerics, device: .mockAny()), + .ddAPIKeyHeader(clientToken: .mockRandom(among: .alphanumerics)), + .ddEVPOriginHeader(source: .mockRandom(among: .alphanumerics)), + .ddEVPOriginVersionHeader(), + .ddRequestIDHeader() + ] + return all.randomElement()! + } + + static func mockAny() -> RequestBuilder.HTTPHeader { + return .ddEVPOriginVersionHeader() + } +} + +extension RequestBuilder: AnyMockable { + static func mockAny() -> RequestBuilder { + return mockWith() + } + + static func mockWith( + url: URL = .mockAny(), + queryItems: [QueryItem] = [], + headers: [HTTPHeader] = [] + ) -> RequestBuilder { + return RequestBuilder(url: url, queryItems: queryItems, headers: headers) } } @@ -655,9 +694,44 @@ extension HTTPClient { } } -extension HTTPHeaders { - static func mockAny() -> HTTPHeaders { - return HTTPHeaders(headers: []) +class NoOpDataUploadWorker: DataUploadWorkerType { + func flushSynchronously() {} + func cancelSynchronously() {} +} + +struct DataUploaderMock: DataUploaderType { + let uploadStatus: DataUploadStatus + + var onUpload: (() -> Void)? = nil + + func upload(data: Data) -> DataUploadStatus { + onUpload?() + return uploadStatus + } +} + +extension DataUploadStatus: RandomMockable { + static func mockRandom() -> DataUploadStatus { + return DataUploadStatus( + needsRetry: .random(), + userDebugDescription: .mockRandom(), + userErrorMessage: .mockRandom(), + internalMonitoringError: (.mockRandom(), ErrorMock(), .mockRandom()) + ) + } + + static func mockWith( + needsRetry: Bool = .mockAny(), + userDebugDescription: String = .mockAny(), + userErrorMessage: String? = nil, + internalMonitoringError: (message: String, error: Error?, attributes: [String: String]?)? = nil + ) -> DataUploadStatus { + return DataUploadStatus( + needsRetry: needsRetry, + userDebugDescription: userDebugDescription, + userErrorMessage: userErrorMessage, + internalMonitoringError: internalMonitoringError + ) } } diff --git a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift index 658181f59c..0e6939d1fe 100644 --- a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift @@ -160,7 +160,7 @@ extension URL: AnyMockable, RandomMockable { return URL(string: "https://www.foo.com/")! .appendingPathComponent( .mockRandom( - among: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + among: .alphanumerics, length: 32 ) ) @@ -170,7 +170,7 @@ extension URL: AnyMockable, RandomMockable { let count: Int = .mockRandom(min: 2, max: 10) var components: [String] = (0.. String { return mockRandom( - among: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ", + among: .alphanumerics + " ", length: length ) } @@ -203,6 +203,9 @@ extension String: AnyMockable, RandomMockable { let characters = (0.. String { return Bool.random() ? self.lowercased() : self.uppercased() } + + static let uuidRegex = "^[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}$" + + func matches(regex: String) -> Bool { + range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil + } } extension Data {