From 4a9e86063df1fa68a10519c20528cc81999a9c38 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Wed, 12 Mar 2025 20:08:26 +0100 Subject: [PATCH 1/8] RUM-8042 Replace Upload Quality with Batch Blocked telemetry --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 +-- .../Core/Upload/DataUploadWorker.swift | 79 +++++++++---------- .../Sources/SDKMetrics/BatchMetrics.swift | 18 +++++ .../Core/Upload/DataUploadWorkerTests.swift | 46 ++++++----- ...tyMetric.swift => UploadCycleMetric.swift} | 10 +-- .../Integrations/TelemetryInterceptor.swift | 10 +-- .../SDKMetrics/SessionEndedMetric.swift | 59 +++----------- .../SessionEndedMetricController.swift | 4 +- .../TelemetryInterceptorTests.swift | 4 +- .../SessionEndedMetricControllerTests.swift | 2 +- .../SDKMetrics/SessionEndedMetricTests.swift | 37 +++------ 11 files changed, 121 insertions(+), 160 deletions(-) rename DatadogInternal/Sources/SDKMetrics/{UploadQualityMetric.swift => UploadCycleMetric.swift} (67%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index e0a64a086c..1c7e6e9da8 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1095,8 +1095,8 @@ D22743E729DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */; }; D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; - D22789362D64A0D7007E9DB0 /* UploadQualityMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */; }; - D22789372D64A0D7007E9DB0 /* UploadQualityMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */; }; + D22789362D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */; }; + D22789372D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */; }; D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; @@ -3117,7 +3117,7 @@ D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageReceiverTests.swift; sourceTree = ""; }; D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+SessionReplay.swift"; sourceTree = ""; }; D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportReceiverTests.swift; sourceTree = ""; }; - D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadQualityMetric.swift; sourceTree = ""; }; + D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadCycleMetric.swift; sourceTree = ""; }; D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convenience.swift"; sourceTree = ""; }; @@ -5414,7 +5414,7 @@ children = ( 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */, A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */, - D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */, + D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -8942,7 +8942,7 @@ D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, D24EC3D92DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */, E2AA55E72C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, - D22789372D64A0D7007E9DB0 /* UploadQualityMetric.swift in Sources */, + D22789372D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */, D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, @@ -10053,7 +10053,7 @@ D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, D24EC3DA2DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */, E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, - D22789362D64A0D7007E9DB0 /* UploadQualityMetric.swift in Sources */, + D22789362D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */, D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index ca7c6d803c..741a6faf81 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -78,13 +78,14 @@ internal class DataUploadWorker: DataUploadWorkerType { DD.logger.debug("⏳ (\(self.featureName)) Uploading batches...") self.backgroundTaskCoordinator?.beginBackgroundTask() self.uploadFile(from: files.reversed(), context: context) + sendUploadCycleMetric() } else { let batchLabel = files?.isEmpty == false ? "YES" : (isSystemReady ? "NO" : "NOT CHECKED") DD.logger.debug("💡 (\(self.featureName)) No upload. Batch to upload: \(batchLabel), System conditions: \(blockersForUpload.description)") self.delay.increase() self.backgroundTaskCoordinator?.endBackgroundTask() self.scheduleNextCycle() - sendUploadQualityMetric(blockers: blockersForUpload) + sendBatchBlockedMetric(blockers: blockersForUpload) } } self.readWork = readWorkItem @@ -121,7 +122,6 @@ internal class DataUploadWorker: DataUploadWorkerType { ) previousUploadStatus = uploadStatus - sendUploadQualityMetric(status: uploadStatus) if uploadStatus.needsRetry { DD.logger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") @@ -147,8 +147,8 @@ internal class DataUploadWorker: DataUploadWorkerType { previousUploadStatus = nil if let error = uploadStatus.error { - // Throw to report the request error accordingly - throw error + sendBatchBlockedMetric(error: error) + throw error // Throw to report the request error accordingly } } catch DataUploadError.httpError(statusCode: .unauthorized), DataUploadError.httpError(statusCode: .forbidden) { DD.logger.error("⚠️ Make sure that the provided token still exists and you're targeting the relevant Datadog site.") @@ -166,7 +166,6 @@ internal class DataUploadWorker: DataUploadWorkerType { self.fileReader.markBatchAsRead(batch, reason: .invalid) previousUploadStatus = nil self.telemetry.error("Failed to initiate '\(self.featureName)' data upload", error: error) - sendUploadQualityMetric(failure: "invalid") } } @@ -235,55 +234,51 @@ internal class DataUploadWorker: DataUploadWorkerType { } } - private func sendUploadQualityMetric(blockers: [DataUploadConditions.Blocker]) { - guard !blockers.isEmpty else { - return sendUploadQualityMetric() - } - - sendUploadQualityMetric( - failure: "blocker", - blockers: blockers.map { - switch $0 { - case .battery: return "low_battery" - case .lowPowerModeOn: return "lpm" - case .networkReachability: return "offline" - } - } + private func sendUploadCycleMetric() { + telemetry.metric( + name: UploadCycleMetric.name, + attributes: [UploadCycleMetric.track: featureName] ) } - private func sendUploadQualityMetric(status: DataUploadStatus) { - guard let error = status.error else { - return sendUploadQualityMetric() + private func sendBatchBlockedMetric(blockers: [DataUploadConditions.Blocker]) { + guard !blockers.isEmpty else { + return } - sendUploadQualityMetric( - failure: { - switch error { - case let .httpError(code): return "\(code)" - case let .networkError(error): return "\(error.code)" - } - }() - ) - } - - private func sendUploadQualityMetric() { telemetry.metric( - name: UploadQualityMetric.name, + name: BatchBlockedMetric.name, attributes: [ - UploadQualityMetric.track: featureName - ] + SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, + BatchMetric.trackKey: featureName, + BatchBlockedMetric.uploaderDelayKey: delay.current, + BatchBlockedMetric.blockers: blockers.map { + switch $0 { + case .battery: return "low_battery" + case .lowPowerModeOn: return "lpm" + case .networkReachability: return "offline" + } + } + ], + sampleRate: BatchBlockedMetric.sampleRate ) } - private func sendUploadQualityMetric(failure: String, blockers: [String] = []) { + private func sendBatchBlockedMetric(error: DataUploadError) { telemetry.metric( - name: UploadQualityMetric.name, + name: BatchBlockedMetric.name, attributes: [ - UploadQualityMetric.track: featureName, - UploadQualityMetric.failure: failure, - UploadQualityMetric.blockers: blockers - ] + SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, + BatchMetric.trackKey: featureName, + BatchBlockedMetric.uploaderDelayKey: delay.current, + BatchBlockedMetric.failure: { + switch error { + case let .httpError(code): return "intake-code-\(code.rawValue)" + case let .networkError(error): return "network-code-\(error.code)" + } + }() + ], + sampleRate: BatchBlockedMetric.sampleRate ) } } diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 70e2bc58bc..fa2f01c59f 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -130,3 +130,21 @@ internal enum BatchClosedMetric { /// If the batch was closed by core or after new batch was forced by the feature. static let forcedNewKey = "forced_new" } + +/// Definition of "Batch Blocked" telemetry. +internal enum BatchBlockedMetric { + /// The name of this metric, included in telemetry log. + /// Note: the "[Mobile Metric]" prefix is added when sending this telemetry in RUM. + static let name = "Batch Blocked" + /// Metric type value. + static let typeValue = "batch blocked" + /// The sample rate for this metric. + /// It is applied in addition to the telemetry sample rate (20% by default). + static let sampleRate: Float = 1.5 // 1.5% + /// The key for uploader's current delay. + static let uploaderDelayKey = "uploader_delay" + + /// List of upload blockers + static let blockers = "blockers" + static let failure = "failure" +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 4289d8b158..f0949e3775 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -89,10 +89,8 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 0) XCTAssertEqual(telemetry.messages.count, 3) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, featureName) - XCTAssertNil(metric.attributes["failure"]) - XCTAssertNil(metric.attributes["blockers"]) } func testItUploadsDataSequentiallyWithoutDelay_whenMaxBatchesPerUploadIsSet() throws { @@ -141,11 +139,9 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() XCTAssertEqual(try orchestrator.directory.files().count, 1) - XCTAssertEqual(telemetry.messages.count, 2) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") + XCTAssertEqual(telemetry.messages.count, 1) + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, featureName) - XCTAssertNil(metric.attributes["failure"]) - XCTAssertNil(metric.attributes["blockers"]) } func testGivenDataToUpload_whenUploadFinishesAndDoesNotNeedToBeRetried_thenDataIsDeleted() { @@ -526,7 +522,7 @@ class DataUploadWorkerTests: XCTestCase { ) } - func testWhenUploadIsBlocked_itDoesSendUploadQualityTelemetry() throws { + func testWhenUploadIsBlocked_itDoesSendBatchBlockedTelemetry() throws { // Given let telemetry = TelemetryMock() @@ -566,9 +562,9 @@ class DataUploadWorkerTests: XCTestCase { // Then XCTAssertEqual(telemetry.messages.count, 1) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "blocker") + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch blocked metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["blockers"] as? [String], ["offline", "low_battery"]) + XCTAssertNil(metric.attributes["failure"]) XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -613,9 +609,12 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 1) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "\(randomStatusCode)") + XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") + XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") + XCTAssertNil(metric.attributes["blockers"]) XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -660,13 +659,16 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(telemetry.messages.count, 3) + + XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message,"Data upload finished with status code: \(randomStatusCode.rawValue)") - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "\(randomStatusCode)") + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") + XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") + XCTAssertNil(metric.attributes["blockers"]) XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -705,13 +707,16 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(telemetry.messages.count, 3) + + XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message, #"Data upload finished with error - Error Domain=abc Code=0 "(null)""#) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "\(nserror.code)") + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") + XCTAssertEqual(metric.attributes["failure"] as? String, "network-code-\(nserror.code)") + XCTAssertNil(metric.attributes["blockers"]) XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -751,8 +756,7 @@ class DataUploadWorkerTests: XCTestCase { let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message, #"Failed to initiate 'some-feature' data upload - Failed to prepare upload"#) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_quality"), "An upload quality metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "invalid") + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, "some-feature") } diff --git a/DatadogInternal/Sources/SDKMetrics/UploadQualityMetric.swift b/DatadogInternal/Sources/SDKMetrics/UploadCycleMetric.swift similarity index 67% rename from DatadogInternal/Sources/SDKMetrics/UploadQualityMetric.swift rename to DatadogInternal/Sources/SDKMetrics/UploadCycleMetric.swift index ea5a634ee0..244c0ab3dc 100644 --- a/DatadogInternal/Sources/SDKMetrics/UploadQualityMetric.swift +++ b/DatadogInternal/Sources/SDKMetrics/UploadCycleMetric.swift @@ -6,18 +6,14 @@ import Foundation -/// Fields of the Upload Quality Metric. +/// Fields of the Upload Cycle Metric. /// /// This metric is not sent to Telemetry as-is, values are sent on the message-bus /// and aggregated internally by RUM's message receiver. The aggregate is sent as an /// attribute of the "RUM Session Ended" metric. -public enum UploadQualityMetric { +public enum UploadCycleMetric { /// Metric's name - public static let name = "upload_quality" + public static let name = "upload_cycle" /// The Metrics' upload track, or feature name. public static let track = "track" - /// The upload's failure description. - public static let failure = "failure" - /// The upload's blockers list. - public static let blockers = "blockers" } diff --git a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift index 3786f73dda..acc7a91989 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift @@ -20,10 +20,10 @@ internal struct TelemetryInterceptor: FeatureMessageReceiver { switch telemetry { case .error(let id, let message, let kind, let stack): interceptError(id: id, message: message, kind: kind, stack: stack) - case .metric(let metric) where metric.name == UploadQualityMetric.name: - // Intercept the 'upload_quality' metric for aggregation in the rse + case .metric(let metric) where metric.name == UploadCycleMetric.name: + // Intercept the 'upload_cycle' metric for aggregation in the rse // metric - interceptUploadQualityMetric(attributes: metric.attributes) + interceptUploadCycleMetric(attributes: metric.attributes) return true // do not forward the message default: @@ -37,7 +37,7 @@ internal struct TelemetryInterceptor: FeatureMessageReceiver { sessionEndedMetric.track(sdkErrorKind: kind, in: nil) // `nil` - track in current session } - private func interceptUploadQualityMetric(attributes: [String: Encodable]) { - sessionEndedMetric.track(uploadQuality: attributes, in: nil) // `nil` - track in current session + private func interceptUploadCycleMetric(attributes: [String: Encodable]) { + sessionEndedMetric.track(uploadCycle: attributes, in: nil) // `nil` - track in current session } } diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 142da312cc..ae10029ddf 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -95,8 +95,8 @@ internal class SessionEndedMetric { /// Indicates if the session was stopped through `stopSession()` API. private var wasStopped = false - /// Information about the upload quality during the session. - private var uploadQuality: [String: Attributes.UploadQuality] = [:] + /// Information about the upload cycle during the session. + private var uploadCycle: [String: Int] = [:] /// If `RUM.Configuration.trackBackgroundEvents` was enabled for this session. private let tracksBackgroundEvents: Bool @@ -203,37 +203,12 @@ internal class SessionEndedMetric { /// /// - Parameters: /// - attributes: The upload quality attributes - func track(uploadQuality attributes: [String: Encodable]) { - guard let track = attributes[UploadQualityMetric.track] as? String else { + func track(uploadCycle attributes: [String: Encodable]) { + guard let track = attributes[UploadCycleMetric.track] as? String else { return } - let uploadQuality = self.uploadQuality[track] ?? Attributes.UploadQuality( - cycleCount: 0, - failureCount: [:], - blockerCount: [:] - ) - - var failureCount = uploadQuality.failureCount - var blockerCount = uploadQuality.blockerCount - - if let failure = attributes[UploadQualityMetric.failure] as? String { - // Merge by incrementing values - failureCount.merge([failure: 1], uniquingKeysWith: +) - } - - if let blockers = attributes[UploadQualityMetric.blockers] as? [String] { - // Merge by incrementing values - blockerCount = blockers.reduce(into: blockerCount) { count, blocker in - count[blocker, default: 0] += 1 - } - } - - self.uploadQuality[track] = Attributes.UploadQuality( - cycleCount: uploadQuality.cycleCount + 1, - failureCount: failureCount, - blockerCount: blockerCount - ) + uploadCycle[track, default: 0] += 1 } // MARK: - Exporting Attributes @@ -347,23 +322,9 @@ internal class SessionEndedMetric { /// Information on number of events missed due to absence of an active view. let noViewEventsCount: NoViewEventsCount - struct UploadQuality: Encodable { - let cycleCount: Int - let failureCount: [String: Int] - let blockerCount: [String: Int] - - enum CodingKeys: String, CodingKey { - case cycleCount = "cycle_count" - case failureCount = "failure_count" - case blockerCount = "blocker_count" - } - } - - /// Information about the upload quality during the session. - /// The upload quality is splitting between upload track name. - /// Tracks upload quality during the session, aggregating them by track name. - /// Each track reports its own upload quality metrics. - let uploadQuality: [String: UploadQuality] + /// Information about the upload cycles during the session. + /// The upload cycles is splitting between upload track name. + let uploadCycle: [String: Int] enum CodingKeys: String, CodingKey { case processType = "process_type" @@ -376,7 +337,7 @@ internal class SessionEndedMetric { case sdkErrorsCount = "sdk_errors_count" case ntpOffset = "ntp_offset" case noViewEventsCount = "no_view_events_count" - case uploadQuality = "upload_quality" + case uploadCycle = "upload_cycle" } } @@ -449,7 +410,7 @@ internal class SessionEndedMetric { errors: missedEvents[.error] ?? 0, longTasks: missedEvents[.longTask] ?? 0 ), - uploadQuality: uploadQuality + uploadCycle: uploadCycle ) ] } diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index c5f4a7ab77..1ad24917e2 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -121,8 +121,8 @@ internal final class SessionEndedMetricController { /// /// - Parameters: /// - attributes: The upload quality attributes - func track(uploadQuality attributes: [String: Encodable], in sessionID: RUMUUID?) { - updateMetric(for: sessionID) { $0?.track(uploadQuality: attributes) } + func track(uploadCycle attributes: [String: Encodable], in sessionID: RUMUUID?) { + updateMetric(for: sessionID) { $0?.track(uploadCycle: attributes) } } private func updateMetric(for sessionID: RUMUUID?, _ mutation: (inout SessionEndedMetric?) throws -> Void) { diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 685cc32bb5..8f11a2033f 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -41,7 +41,7 @@ class TelemetryInterceptorTests: XCTestCase { // When metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny(), tracksBackgroundEvents: .mockRandom()) - let metricTelemetry: TelemetryMessage = .metric(MetricTelemetry(name: UploadQualityMetric.name, attributes: [UploadQualityMetric.track: "feature"], sampleRate: .mockRandom())) + let metricTelemetry: TelemetryMessage = .metric(MetricTelemetry(name: UploadCycleMetric.name, attributes: [UploadCycleMetric.track: "feature"], sampleRate: .mockRandom())) let result = interceptor.receive(message: .telemetry(metricTelemetry), from: NOPDatadogCore()) XCTAssertTrue(result) @@ -49,6 +49,6 @@ class TelemetryInterceptorTests: XCTestCase { metricController.endMetric(sessionID: sessionID, with: .mockRandom()) let metric = try XCTUnwrap(telemetry.messages.lastMetric(named: SessionEndedMetric.Constants.name)) let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) - XCTAssertEqual(rse.uploadQuality["feature"]?.cycleCount, 1) + XCTAssertEqual(rse.uploadCycle["feature"], 1) } } diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index 8e325b5ced..9089e668eb 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -114,7 +114,7 @@ class SessionEndedMetricControllerTests: XCTestCase { { controller.track(missedEventType: .action, in: sessionIDs.randomElement()!) }, { controller.track(missedEventType: .resource, in: nil) }, { controller.trackWasStopped(sessionID: nil) }, - { controller.track(uploadQuality: mockRandomAttributes(), in: nil) }, + { controller.track(uploadCycle: mockRandomAttributes(), in: nil) }, { controller.endMetric(sessionID: sessionIDs.randomElement()!, with: .mockRandom()) }, ], iterations: 100 diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index d4eca43a33..9eb21b51f0 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -593,35 +593,22 @@ class SessionEndedMetricTests: XCTestCase { func testUploadQualityMetricAggregation() throws { let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSApp)) - metric.track(uploadQuality: [ - UploadQualityMetric.track: "feature", - UploadQualityMetric.failure: "error1" - ]) - metric.track(uploadQuality: [ - UploadQualityMetric.track: "feature", - UploadQualityMetric.failure: "error1", - UploadQualityMetric.blockers: ["blocker1", "blocker2"] - ]) - - metric.track(uploadQuality: [ - UploadQualityMetric.track: "feature", - ]) + let count1: Int = .mockRandom(min: 10, max: 100) + for _ in 0.. Date: Fri, 14 Mar 2025 17:58:36 +0100 Subject: [PATCH 2/8] RUM-8042 Add count of batches blocked --- .../Core/Upload/DataUploadWorker.swift | 23 ++++--- .../Sources/SDKMetrics/BatchMetrics.swift | 4 +- .../Core/Upload/DataUploadWorkerTests.swift | 62 +++++++++++++------ 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index 741a6faf81..68a9edd9f9 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -73,21 +73,23 @@ internal class DataUploadWorker: DataUploadWorkerType { let context = contextProvider.read() let blockersForUpload = uploadConditions.blockersForUpload(with: context) let isSystemReady = blockersForUpload.isEmpty - let files = isSystemReady ? fileReader.readFiles(limit: maxBatchesPerUpload) : nil - if let files = files, !files.isEmpty { + let files = fileReader.readFiles(limit: maxBatchesPerUpload) + + if !files.isEmpty && isSystemReady { DD.logger.debug("⏳ (\(self.featureName)) Uploading batches...") self.backgroundTaskCoordinator?.beginBackgroundTask() self.uploadFile(from: files.reversed(), context: context) sendUploadCycleMetric() } else { - let batchLabel = files?.isEmpty == false ? "YES" : (isSystemReady ? "NO" : "NOT CHECKED") + let batchLabel = files.isEmpty ? "NO" : "YES" DD.logger.debug("💡 (\(self.featureName)) No upload. Batch to upload: \(batchLabel), System conditions: \(blockersForUpload.description)") self.delay.increase() self.backgroundTaskCoordinator?.endBackgroundTask() self.scheduleNextCycle() - sendBatchBlockedMetric(blockers: blockersForUpload) + sendBatchBlockedMetric(blockers: blockersForUpload, batchCount: files.count) } } + self.readWork = readWorkItem // Start sending batches immediately after initialization: @@ -107,6 +109,7 @@ internal class DataUploadWorker: DataUploadWorkerType { return } + let filesCount = files.count var files = files guard let file = files.popLast() else { self.scheduleNextCycle() @@ -127,6 +130,7 @@ internal class DataUploadWorker: DataUploadWorkerType { DD.logger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") self.delay.increase() self.scheduleNextCycle() + sendBatchBlockedMetric(status: uploadStatus, batchCount: filesCount) return } @@ -147,7 +151,6 @@ internal class DataUploadWorker: DataUploadWorkerType { previousUploadStatus = nil if let error = uploadStatus.error { - sendBatchBlockedMetric(error: error) throw error // Throw to report the request error accordingly } } catch DataUploadError.httpError(statusCode: .unauthorized), DataUploadError.httpError(statusCode: .forbidden) { @@ -241,7 +244,7 @@ internal class DataUploadWorker: DataUploadWorkerType { ) } - private func sendBatchBlockedMetric(blockers: [DataUploadConditions.Blocker]) { + private func sendBatchBlockedMetric(blockers: [DataUploadConditions.Blocker], batchCount: Int) { guard !blockers.isEmpty else { return } @@ -252,6 +255,7 @@ internal class DataUploadWorker: DataUploadWorkerType { SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, BatchMetric.trackKey: featureName, BatchBlockedMetric.uploaderDelayKey: delay.current, + BatchBlockedMetric.batchCount: batchCount, BatchBlockedMetric.blockers: blockers.map { switch $0 { case .battery: return "low_battery" @@ -264,13 +268,18 @@ internal class DataUploadWorker: DataUploadWorkerType { ) } - private func sendBatchBlockedMetric(error: DataUploadError) { + private func sendBatchBlockedMetric(status: DataUploadStatus, batchCount: Int) { + guard let error = status.error else { + return + } + telemetry.metric( name: BatchBlockedMetric.name, attributes: [ SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, BatchMetric.trackKey: featureName, BatchBlockedMetric.uploaderDelayKey: delay.current, + BatchBlockedMetric.batchCount: batchCount, BatchBlockedMetric.failure: { switch error { case let .httpError(code): return "intake-code-\(code.rawValue)" diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index fa2f01c59f..90585218e8 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -142,7 +142,9 @@ internal enum BatchBlockedMetric { /// It is applied in addition to the telemetry sample rate (20% by default). static let sampleRate: Float = 1.5 // 1.5% /// The key for uploader's current delay. - static let uploaderDelayKey = "uploader_delay" + static let uploaderDelayKey = "uploader_delay.current" + /// The key for count of bacthes being blocked. + static let batchCount = "batch_count" /// List of upload blockers static let blockers = "blockers" diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index f0949e3775..1e59be99eb 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -609,13 +609,8 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(telemetry.messages.count, 1) XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") - - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") - XCTAssertNil(metric.attributes["blockers"]) - XCTAssertEqual(metric.attributes["track"] as? String, featureName) } func testWhenDataIsUploadedWithAlertingStatusCode_itSendsErrorTelemetry() throws { @@ -659,17 +654,9 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 3) - - XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message,"Data upload finished with status code: \(randomStatusCode.rawValue)") - - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") - XCTAssertNil(metric.attributes["blockers"]) - XCTAssertEqual(metric.attributes["track"] as? String, featureName) } func testWhenDataCannotBeUploadedDueToNetworkError_itSendsErrorTelemetry() throws { @@ -707,15 +694,52 @@ class DataUploadWorkerTests: XCTestCase { worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 3) - - XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") - let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message, #"Data upload finished with error - Error Domain=abc Code=0 "(null)""#) + } + + func testWhenDataIsUploadedWithRetryableStatusCode_itSendsBatchBlockedTelemetry() throws { + // Given + let telemetry = TelemetryMock() + + writer.write(value: ["key": "value"]) + let randomStatusCode: HTTPResponseStatusCode = [ + .requestTimeout, + .tooManyRequests, + .internalServerError, + .serviceUnavailable + ].randomElement()! + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + let mockDataUploader = DataUploaderMock( + uploadStatus: .mockWith(needsRetry: true, error: .httpError(statusCode: randomStatusCode)) + ) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } + + let featureName: String = .mockRandom() + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: featureName, + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "network-code-\(nserror.code)") + XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") XCTAssertNil(metric.attributes["blockers"]) XCTAssertEqual(metric.attributes["track"] as? String, featureName) } From 482f5df80656a079aebecfd7794052846cf73a39 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Wed, 26 Mar 2025 18:29:53 +0100 Subject: [PATCH 3/8] RUM-8042 Batch Blocked aggregator --- Datadog/Datadog.xcodeproj/project.pbxproj | 14 ++- DatadogCore/Sources/Core/DatadogCore.swift | 31 +++++- DatadogCore/Sources/Core/MessageBus.swift | 5 +- .../Core/Upload/DataUploadWorker.swift | 55 +++++------ .../Sources/Core/Upload/FeatureUpload.swift | 6 +- DatadogCore/Sources/Datadog.swift | 3 +- .../BatchBlockedMetricAggregator.swift | 73 ++++++++++++++ .../Sources/SDKMetrics/BatchMetrics.swift | 11 +-- .../Core/Upload/DataUploadWorkerTests.swift | 31 +++--- .../BatchBlockedMetricAggregatorTests.swift | 97 +++++++++++++++++++ .../Sources/Telemetry/Telemetry.swift | 16 +++ 11 files changed, 279 insertions(+), 63 deletions(-) create mode 100644 DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift create mode 100644 DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 1c7e6e9da8..fb4cb08450 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1485,6 +1485,10 @@ D2A7A9002BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9022BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9032BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; + D2AB80BD2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */; }; + D2AB80BE2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */; }; + D2AB80DF2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */; }; + D2AB80E02D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */; }; D2AD1CC32CE4AE6600106C74 /* Color+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */; }; D2AD1CC42CE4AE6600106C74 /* DisplayList+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBD2CE4AE6600106C74 /* DisplayList+Reflection.swift */; }; D2AD1CC52CE4AE6600106C74 /* DisplayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */; }; @@ -3249,6 +3253,8 @@ D2A7840129A534F9003B03BB /* DatadogLogsTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogLogsTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; + D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchBlockedMetricAggregator.swift; sourceTree = ""; }; + D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchBlockedMetricAggregatorTests.swift; sourceTree = ""; }; D2AD1CB92CE4AE6600106C74 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Reflection.swift"; sourceTree = ""; }; D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayList.swift; sourceTree = ""; }; @@ -5395,8 +5401,9 @@ 6174D6082BFDDD1E00EC7469 /* SDKMetrics */ = { isa = PBXGroup; children = ( - D2E6E8FA2D8039B200FF1398 /* BenchmarkURLSessionTaskDelegate.swift */, 614396712A67D74F00197326 /* BatchMetrics.swift */, + D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */, + D2E6E8FA2D8039B200FF1398 /* BenchmarkURLSessionTaskDelegate.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -5405,6 +5412,7 @@ isa = PBXGroup; children = ( 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */, + D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -8411,6 +8419,7 @@ D29CDD3228211A2200F7DAA5 /* TLVBlock.swift in Sources */, 6128F5742BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, D2553829288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */, + D2AB80BE2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */, D224430629E95C2C00274EC7 /* MessageBus.swift in Sources */, 61F930BE2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, 6128F5772BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, @@ -8530,6 +8539,7 @@ 61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */, 61133C612423990D00786299 /* URLSessionClientTests.swift in Sources */, 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, + D2AB80DF2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */, D22743DB29DEB8B4001A7EF9 /* VitalCPUReaderTests.swift in Sources */, 61DA8CAC2861C3720074A606 /* DirectoriesTests.swift in Sources */, 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */, @@ -9792,6 +9802,7 @@ 6128F5752BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, D2303A0B298D5412001A1FA3 /* AsyncWriter.swift in Sources */, D224430729E95C2E00274EC7 /* MessageBus.swift in Sources */, + D2AB80BD2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */, 61F930BF2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, 6128F5782BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, D2CB6E3627C50EAE00A62B57 /* ObjcAppLaunchHandler.m in Sources */, @@ -9855,6 +9866,7 @@ 61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */, D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */, + D2AB80E02D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */, 61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */, D2DC4BBD27F234E000E4FB96 /* CITestIntegrationTests.swift in Sources */, D2CB6EE427C520D400A62B57 /* FeatureTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index ac8bb31faa..858dedab74 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -75,6 +75,10 @@ internal final class DatadogCore { /// Maximum number of batches per upload. internal let maxBatchesPerUpload: Int + /// Instance to aggregate batch-blocked metric to be sent when the + /// application goes to background. + private let batchBlockedMetricAggregator = BatchBlockedMetricAggregator() + /// Creates a core instance. /// /// - Parameters: @@ -86,6 +90,10 @@ internal final class DatadogCore { /// - encryption: The on-disk data encryption. /// - contextProvider: The core context provider. /// - applicationVersion: The application version. + /// - maxBatchesPerUpload: Number of batch to process during an upload cycle. + /// - backgroundTasksEnabled: Enables upload background task. + /// - isRunFromExtension: Set `true` when the SDK is initialised from an extension. + /// - notificationCenter: The Notification center to observe. init( directory: CoreDirectory, dateProvider: DateProvider, @@ -97,7 +105,8 @@ internal final class DatadogCore { applicationVersion: String, maxBatchesPerUpload: Int, backgroundTasksEnabled: Bool, - isRunFromExtension: Bool = false + isRunFromExtension: Bool = false, + notificationCenter: NotificationCenter = .default ) { self.directory = directory self.dateProvider = dateProvider @@ -123,6 +132,14 @@ internal final class DatadogCore { self.contextProvider.publish { [weak self] context in self?.send(message: .context(context)) } + + // observe application entering background + notificationCenter.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: ApplicationNotifications.didEnterBackground, + object: nil + ) } /// Sets current user information. @@ -309,6 +326,15 @@ internal final class DatadogCore { stores = [:] features = [:] } + + @objc + private func applicationDidEnterBackground() { + // Report aggregated 'Batch Blocked' telemetry metric + // when the application enters background. + for metric in batchBlockedMetricAggregator.flush() { + telemetry.send(telemetry: .metric(metric)) + } + } } extension DatadogCore: DatadogCoreProtocol { @@ -352,7 +378,8 @@ extension DatadogCore: DatadogCoreProtocol { performance: performancePreset, backgroundTasksEnabled: backgroundTasksEnabled, isRunFromExtension: isRunFromExtension, - telemetry: telemetry + telemetry: telemetry, + batchBlockedMetricAggregator: batchBlockedMetricAggregator ) stores[T.name] = ( diff --git a/DatadogCore/Sources/Core/MessageBus.swift b/DatadogCore/Sources/Core/MessageBus.swift index c264236fb6..c87f0c9ef9 100644 --- a/DatadogCore/Sources/Core/MessageBus.swift +++ b/DatadogCore/Sources/Core/MessageBus.swift @@ -88,9 +88,8 @@ internal final class MessageBus { /// - fallback: The fallback closure to call when the message could not be /// processed by any Features on the bus. func send(message: FeatureMessage, else fallback: @escaping () -> Void = {}) { - if // Configuration Telemetry Message - case .telemetry(let telemetry) = message, - case .configuration(let configuration) = telemetry { + if case .telemetry(.configuration(let configuration) ) = message { + // Configuration Telemetry Message return save(configuration: configuration) } diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index 68a9edd9f9..c0c92cba96 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -44,6 +44,8 @@ internal class DataUploadWorker: DataUploadWorkerType { private var previousUploadStatus: DataUploadStatus? + private let batchBlockedMetricAggregator: BatchBlockedMetricAggregator? + init( queue: DispatchQueue, fileReader: Reader, @@ -54,7 +56,8 @@ internal class DataUploadWorker: DataUploadWorkerType { featureName: String, telemetry: Telemetry, maxBatchesPerUpload: Int, - backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil + backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil, + batchBlockedMetricAggregator: BatchBlockedMetricAggregator? = nil ) { self.queue = queue self.fileReader = fileReader @@ -65,6 +68,8 @@ internal class DataUploadWorker: DataUploadWorkerType { self.delay = delay self.featureName = featureName self.telemetry = telemetry + self.batchBlockedMetricAggregator = batchBlockedMetricAggregator + let readWorkItem = DispatchWorkItem { [weak self] in guard let self = self else { return @@ -249,22 +254,16 @@ internal class DataUploadWorker: DataUploadWorkerType { return } - telemetry.metric( - name: BatchBlockedMetric.name, - attributes: [ - SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, - BatchMetric.trackKey: featureName, - BatchBlockedMetric.uploaderDelayKey: delay.current, - BatchBlockedMetric.batchCount: batchCount, - BatchBlockedMetric.blockers: blockers.map { - switch $0 { - case .battery: return "low_battery" - case .lowPowerModeOn: return "lpm" - case .networkReachability: return "offline" - } + batchBlockedMetricAggregator?.increment( + by: batchCount, + track: featureName, + blockers: blockers.map { + switch $0 { + case .battery: return "low_battery" + case .lowPowerModeOn: return "lpm" + case .networkReachability: return "offline" } - ], - sampleRate: BatchBlockedMetric.sampleRate + } ) } @@ -273,21 +272,15 @@ internal class DataUploadWorker: DataUploadWorkerType { return } - telemetry.metric( - name: BatchBlockedMetric.name, - attributes: [ - SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, - BatchMetric.trackKey: featureName, - BatchBlockedMetric.uploaderDelayKey: delay.current, - BatchBlockedMetric.batchCount: batchCount, - BatchBlockedMetric.failure: { - switch error { - case let .httpError(code): return "intake-code-\(code.rawValue)" - case let .networkError(error): return "network-code-\(error.code)" - } - }() - ], - sampleRate: BatchBlockedMetric.sampleRate + batchBlockedMetricAggregator?.increment( + by: batchCount, + track: featureName, + failure: { + switch error { + case let .httpError(code): return "intake-code-\(code.rawValue)" + case let .networkError(error): return "network-code-\(error.code)" + } + }() ) } } diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift index 401cb0a001..7071ac18e9 100644 --- a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift @@ -20,7 +20,8 @@ internal struct FeatureUpload { performance: PerformancePreset, backgroundTasksEnabled: Bool, isRunFromExtension: Bool, - telemetry: Telemetry + telemetry: Telemetry, + batchBlockedMetricAggregator: BatchBlockedMetricAggregator? = nil ) { let uploadQueue = DispatchQueue( label: "com.datadoghq.ios-sdk-\(featureName)-upload", @@ -63,7 +64,8 @@ internal struct FeatureUpload { featureName: featureName, telemetry: telemetry, maxBatchesPerUpload: performance.maxBatchesPerUpload, - backgroundTaskCoordinator: backgroundTaskCoordinator + backgroundTaskCoordinator: backgroundTaskCoordinator, + batchBlockedMetricAggregator: batchBlockedMetricAggregator ) ) } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 23f43cc700..25cded3786 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -661,7 +661,8 @@ extension DatadogCore { applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, backgroundTasksEnabled: configuration.backgroundTasksEnabled, - isRunFromExtension: isRunFromExtension + isRunFromExtension: isRunFromExtension, + notificationCenter: configuration.notificationCenter ) telemetry.configuration( diff --git a/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift b/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift new file mode 100644 index 0000000000..cc87bdcba3 --- /dev/null +++ b/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift @@ -0,0 +1,73 @@ +/* + * 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-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +internal final class BatchBlockedMetricAggregator { + private struct AggregationKey: Hashable { + let track: String + let failure: String? + let blockers: [String]? + } + + let sampleRate: SampleRate + + @ReadWriteLock + private var aggregations: [AggregationKey: Int] = [:] + + init(sampleRate: SampleRate = MetricTelemetry.defaultSampleRate) { + self.sampleRate = sampleRate + } + + func increment(by count: Int, track: String, failure: String) { + increment(by: count, key: AggregationKey(track: track, failure: failure, blockers: nil)) + } + + func increment(by count: Int, track: String, blockers: [String]) { + increment(by: count, key: AggregationKey(track: track, failure: nil, blockers: blockers)) + } + + private func increment(by count: Int, key: AggregationKey) { + _aggregations.mutate { $0[key, default: 0] += count } + } + + func flush() -> [MetricTelemetry] { + _aggregations.mutate { aggregations in + defer { aggregations = [:] } + + return aggregations.compactMap { key, value in + if let failure = key.failure { + return MetricTelemetry( + name: BatchBlockedMetric.name, + attributes: [ + SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, + BatchMetric.trackKey: key.track, + BatchBlockedMetric.batchCount: value, + BatchBlockedMetric.failure: failure + ], + sampleRate: sampleRate + ) + } + + if let blockers = key.blockers { + return MetricTelemetry( + name: BatchBlockedMetric.name, + attributes: [ + SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, + BatchMetric.trackKey: key.track, + BatchBlockedMetric.batchCount: value, + BatchBlockedMetric.blockers: blockers + ], + sampleRate: sampleRate + ) + } + + return nil + } + } + } +} diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 90585218e8..a7ae907887 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -138,15 +138,10 @@ internal enum BatchBlockedMetric { static let name = "Batch Blocked" /// Metric type value. static let typeValue = "batch blocked" - /// The sample rate for this metric. - /// It is applied in addition to the telemetry sample rate (20% by default). - static let sampleRate: Float = 1.5 // 1.5% - /// The key for uploader's current delay. - static let uploaderDelayKey = "uploader_delay.current" /// The key for count of bacthes being blocked. - static let batchCount = "batch_count" - - /// List of upload blockers + static let batchCount = "count" + /// List of upload blocker reasons static let blockers = "blockers" + /// The blocking failure reason. static let failure = "failure" } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 1e59be99eb..f2b1dc4fed 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -524,7 +524,7 @@ class DataUploadWorkerTests: XCTestCase { func testWhenUploadIsBlocked_itDoesSendBatchBlockedTelemetry() throws { // Given - let telemetry = TelemetryMock() + let aggregator = BatchBlockedMetricAggregator() // When let uploadExpectation = self.expectation(description: "Upload has started") @@ -553,19 +553,19 @@ class DataUploadWorkerTests: XCTestCase { uploadConditions: .neverUpload(), delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), featureName: featureName, - telemetry: telemetry, - maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + batchBlockedMetricAggregator: aggregator ) wait(for: [uploadExpectation], timeout: 0.5) worker.cancelSynchronously() // Then - XCTAssertEqual(telemetry.messages.count, 1) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch blocked metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["blockers"] as? [String], ["offline", "low_battery"]) - XCTAssertNil(metric.attributes["failure"]) - XCTAssertEqual(metric.attributes["track"] as? String, featureName) + let metrics = aggregator.flush() + XCTAssertEqual(metrics.count, 1) + XCTAssertEqual(metrics.first?.attributes["blockers"] as? [String], ["offline", "low_battery"]) + XCTAssertEqual(metrics.first?.attributes["track"] as? String, featureName) } func testWhenDataIsUploadedWithServerError_itDoesNotSendErrorTelemetry() throws { @@ -700,7 +700,7 @@ class DataUploadWorkerTests: XCTestCase { func testWhenDataIsUploadedWithRetryableStatusCode_itSendsBatchBlockedTelemetry() throws { // Given - let telemetry = TelemetryMock() + let aggregator = BatchBlockedMetricAggregator() writer.write(value: ["key": "value"]) let randomStatusCode: HTTPResponseStatusCode = [ @@ -730,18 +730,19 @@ class DataUploadWorkerTests: XCTestCase { uploadConditions: .alwaysUpload(), delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), featureName: featureName, - telemetry: telemetry, - maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + batchBlockedMetricAggregator: aggregator ) wait(for: [startUploadExpectation], timeout: 0.5) worker.cancelSynchronously() // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Blocked"), "A Batch Blocked metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") - XCTAssertNil(metric.attributes["blockers"]) - XCTAssertEqual(metric.attributes["track"] as? String, featureName) + let metrics = aggregator.flush() + XCTAssertEqual(metrics.count, 1) + XCTAssertEqual(metrics.first?.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") + XCTAssertEqual(metrics.first?.attributes["track"] as? String, featureName) } func testWhenDataCannotBePreparedForUpload_itSendsErrorTelemetry() throws { diff --git a/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift b/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift new file mode 100644 index 0000000000..8d9f39f7a3 --- /dev/null +++ b/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift @@ -0,0 +1,97 @@ +/* + * 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-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +@testable import DatadogCore + +class BatchBlockedMetricAggregatorTests: XCTestCase { + func testFailureIncrement() throws { + // Given + let aggregator = BatchBlockedMetricAggregator() + + let track1: String = .mockRandom() + let failure1: String = .mockRandom() + + let track2: String = .mockRandom() + let failure2: String = .mockRandom() + + let iterations: Int = .mockRandom(min: 0, max: 100) + + // When + for _ in 0.. Date: Wed, 16 Apr 2025 17:07:56 +0200 Subject: [PATCH 4/8] RUM-8042 Support generic metric telemetry aggregation --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 + DatadogCore/Sources/Core/DatadogCore.swift | 4 +- .../BatchBlockedMetricAggregator.swift | 6 +- .../FilesOrchestrator+MetricsTests.swift | 8 +- .../Sources/Concurrency/ReadWriteLock.swift | 5 +- .../Sources/SDKMetrics/SDKMetricFields.swift | 2 + DatadogInternal/Sources/Storage.swift | 6 + .../Sources/Telemetry/Telemetry.swift | 213 ++++++++++++------ .../Tests/Telemetry/TelemetryTests.swift | 14 +- .../Integrations/TelemetryInterceptor.swift | 2 +- .../Integrations/TelemetryReceiver.swift | 86 ++++++- .../MetricTelemetryAggregator.swift | 49 ++++ .../TelemetryInterceptorTests.swift | 4 +- .../MetricTelemetryAggregatorTests.swift | 104 +++++++++ .../Feature/SessionReplayTelemetryTests.swift | 6 +- .../Recorder/RecordingCoordinatorTests.swift | 2 +- DatadogTrace/Sources/TraceConfiguration.swift | 2 +- .../DatadogInternal/TelemetryMocks.swift | 62 ++++- 18 files changed, 479 insertions(+), 108 deletions(-) create mode 100644 DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift create mode 100644 DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index fb4cb08450..f1d34ce399 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1447,6 +1447,10 @@ D29A9FDB29DDC6D1005C54A4 /* RUMEventFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */; }; D29CDD3228211A2200F7DAA5 /* TLVBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */; }; D29CDD3328211A2200F7DAA5 /* TLVBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */; }; + D2A133912DAFA8B200D84D3C /* MetricTelemetryAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A133902DAFA8B200D84D3C /* MetricTelemetryAggregator.swift */; }; + D2A133922DAFA8B200D84D3C /* MetricTelemetryAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A133902DAFA8B200D84D3C /* MetricTelemetryAggregator.swift */; }; + D2A133942DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A133932DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift */; }; + D2A133952DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A133932DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift */; }; D2A1EE23287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */; }; D2A1EE24287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */; }; D2A1EE32287DA51900D28DFB /* UserInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */; }; @@ -3240,6 +3244,8 @@ D29A9FCB29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTAssertValidRUMUUID.swift; sourceTree = ""; }; D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlock.swift; sourceTree = ""; }; D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIActionModifier.swift; sourceTree = ""; }; + D2A133902DAFA8B200D84D3C /* MetricTelemetryAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTelemetryAggregator.swift; sourceTree = ""; }; + D2A133932DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTelemetryAggregatorTests.swift; sourceTree = ""; }; D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisher.swift; sourceTree = ""; }; D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoPublisher.swift; sourceTree = ""; }; D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisherTests.swift; sourceTree = ""; }; @@ -5435,6 +5441,7 @@ 615E2B8D2D39444300D85243 /* ViewEndedController.swift */, 615E2B942D425F5600D85243 /* ViewEndedMetric.swift */, 11030D752D96EC5300732D5F /* ViewHitchesMetric.swift */, + D2A133902DAFA8B200D84D3C /* MetricTelemetryAggregator.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -5444,6 +5451,7 @@ children = ( 6174D6192BFE449300EC7469 /* SessionEndedMetricTests.swift */, 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */, + D2A133932DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -9083,6 +9091,7 @@ D23F8E5929DDCD28001CFAE8 /* WebViewEventReceiver.swift in Sources */, 265496D32D81C5B10094B6E2 /* RUMAccount.swift in Sources */, D253EE972B988CA90010B589 /* ViewCache.swift in Sources */, + D2A133922DAFA8B200D84D3C /* MetricTelemetryAggregator.swift in Sources */, D23F8E5A29DDCD28001CFAE8 /* RUMResourceScope.swift in Sources */, D23F8E5C29DDCD28001CFAE8 /* RUMApplicationScope.swift in Sources */, 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, @@ -9222,6 +9231,7 @@ D23F8EB329DDCD38001CFAE8 /* ErrorMessageReceiverTests.swift in Sources */, 61C713C12A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, D23F8EB429DDCD38001CFAE8 /* RUMApplicationScopeTests.swift in Sources */, + D2A133942DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift in Sources */, 6105C5152D0C584F00C4C5EE /* INVMetricTests.swift in Sources */, D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */, 61C713CB2A3DC22700FA735A /* RUMTests.swift in Sources */, @@ -9530,6 +9540,7 @@ D29A9F6229DD85BB005C54A4 /* WebViewEventReceiver.swift in Sources */, 265496D42D81C5B10094B6E2 /* RUMAccount.swift in Sources */, D253EE962B988CA90010B589 /* ViewCache.swift in Sources */, + D2A133912DAFA8B200D84D3C /* MetricTelemetryAggregator.swift in Sources */, D29A9F8429DD85BB005C54A4 /* RUMResourceScope.swift in Sources */, D29A9F7329DD85BB005C54A4 /* RUMApplicationScope.swift in Sources */, 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, @@ -9669,6 +9680,7 @@ D29A9FBB29DDB483005C54A4 /* ErrorMessageReceiverTests.swift in Sources */, 61C713C02A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, D29A9F9F29DDB483005C54A4 /* RUMApplicationScopeTests.swift in Sources */, + D2A133952DB1074000D84D3C /* MetricTelemetryAggregatorTests.swift in Sources */, 6105C5142D0C584F00C4C5EE /* INVMetricTests.swift in Sources */, D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */, 61C713CA2A3DC22700FA735A /* RUMTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 858dedab74..f46188cc55 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -331,8 +331,8 @@ internal final class DatadogCore { private func applicationDidEnterBackground() { // Report aggregated 'Batch Blocked' telemetry metric // when the application enters background. - for metric in batchBlockedMetricAggregator.flush() { - telemetry.send(telemetry: .metric(metric)) + for event in batchBlockedMetricAggregator.flush() { + telemetry.metric(.report(event)) } } } diff --git a/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift b/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift index cc87bdcba3..29d53da54a 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift @@ -35,13 +35,13 @@ internal final class BatchBlockedMetricAggregator { _aggregations.mutate { $0[key, default: 0] += count } } - func flush() -> [MetricTelemetry] { + func flush() -> [MetricTelemetry.Event] { _aggregations.mutate { aggregations in defer { aggregations = [:] } return aggregations.compactMap { key, value in if let failure = key.failure { - return MetricTelemetry( + return MetricTelemetry.Event( name: BatchBlockedMetric.name, attributes: [ SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, @@ -54,7 +54,7 @@ internal final class BatchBlockedMetricAggregator { } if let blockers = key.blockers { - return MetricTelemetry( + return MetricTelemetry.Event( name: BatchBlockedMetric.name, attributes: [ SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 04c05b2e58..5930611019 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -63,7 +63,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { orchestrator.delete(readableFile: file, deletionReason: .intakeCode(responseCode: 202)) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) DDAssertJSONEqual(metric.attributes, [ "metric_type": "batch deleted", "track": "track name", @@ -95,7 +95,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { _ = orchestrator.getReadableFiles() // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) DDAssertJSONEqual(metric.attributes, [ "metric_type": "batch deleted", "track": "track name", @@ -130,7 +130,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { _ = try orchestrator.getWritableFile(writeSize: 1) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) DDAssertJSONEqual(metric.attributes, [ "metric_type": "batch deleted", "track": "track name", @@ -175,7 +175,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { _ = try orchestrator.getWritableFile(writeSize: 1) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Closed")) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Closed")) DDAssertReflectionEqual(metric.attributes, [ "metric_type": "batch closed", "track": "track name", diff --git a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift index 8bd3687a8e..2a66de6b7f 100644 --- a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift +++ b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift @@ -51,9 +51,10 @@ public final class ReadWriteLock: @unchecked Sendable { /// The lock will be acquired once for writing before invoking the closure. /// /// - Parameter closure: The closure with the mutable value. - public func mutate(_ closure: (inout Value) throws -> Void) rethrows { + @discardableResult + public func mutate(_ closure: (inout Value) throws -> T) rethrows -> T { pthread_rwlock_wrlock(rwlock) defer { pthread_rwlock_unlock(rwlock) } - try closure(&value) + return try closure(&value) } } diff --git a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift index 25d01e9d6b..e2bdf83033 100644 --- a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift +++ b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift @@ -10,6 +10,8 @@ import Foundation public enum SDKMetricFields { /// Metric type key. It expects `String` value. public static let typeKey = "metric_type" + /// Metric value key. It expects `Double` value. + public static let valueKey = "value" /// The first sample rate applied to the metric. public static let headSampleRate = "head_sample_rate" /// Key referencing the session ID (`String`) that the metric should be sent with. It expects `String` value. diff --git a/DatadogInternal/Sources/Storage.swift b/DatadogInternal/Sources/Storage.swift index e5a51ca0a1..a388ff1f36 100644 --- a/DatadogInternal/Sources/Storage.swift +++ b/DatadogInternal/Sources/Storage.swift @@ -14,6 +14,12 @@ public protocol Storage { func mostRecentModifiedFileAt(before: Date) throws -> Date? } +extension DatadogCoreProtocol { + /// Provides access to the `Storage` associated with the core. + /// - Returns: The `Storage` instance. + public var storage: Storage { CoreStorage(core: self) } +} + internal struct CoreStorage: Storage { /// A weak core reference. private weak var core: DatadogCoreProtocol? diff --git a/DatadogInternal/Sources/Telemetry/Telemetry.swift b/DatadogInternal/Sources/Telemetry/Telemetry.swift index bd65747d47..9d6d7cf85c 100644 --- a/DatadogInternal/Sources/Telemetry/Telemetry.swift +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -65,51 +65,63 @@ public struct ConfigurationTelemetry: Equatable { public let useWorkerUrl: Bool? } -/// A telemetry event that can be sampled in addition to the global telemetry sample rate. -public protocol SampledTelemetry { - /// The sample rate for this metric, applied in addition to the telemetry sample rate. - var sampleRate: SampleRate { get } -} - -public struct MetricTelemetry: SampledTelemetry { +public enum MetricTelemetry { /// The default sample rate for metric events (15%), applied in addition to the telemetry sample rate (20% by default). public static let defaultSampleRate: SampleRate = 15 - /// The name of the metric. - public let name: String + /// Cardinality of a metric used for aggregation. + public typealias Cardinalities = [String: Cardinality] - /// The attributes associated with this metric. - public let attributes: [String: Encodable] + /// Single cardinality of a metric. + public enum Cardinality: Hashable { + case string(String) + case array([Self]) + } - /// The sample rate for this metric, applied in addition to the telemetry sample rate. - /// - /// Must be a value between `0` (reject all) and `100` (keep all). - /// - /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) - /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. - /// - /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). - public let sampleRate: SampleRate - - /// Creates an Metric Telemtry object. - /// - /// - Parameters: - /// - name: The name of the metric. - /// - attributes: The attributes associated with this metric. - /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate. - public init( - name: String, - attributes: [String : Encodable], - sampleRate: SampleRate - ) { - self.name = name - self.attributes = attributes - self.sampleRate = sampleRate + public struct Event { + /// The name of the metric. + public let name: String + + /// The attributes associated with this metric. + public let attributes: [String: Encodable] + + /// The sample rate for this metric, applied in addition to the telemetry sample rate. + /// + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public let sampleRate: SampleRate + + /// Creates an Metric Telemtry object. + /// + /// - Parameters: + /// - name: The name of the metric. + /// - attributes: The attributes associated with this metric. + /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate. + public init( + name: String, + attributes: [String: Encodable], + sampleRate: SampleRate + ) { + self.name = name + self.attributes = attributes + self.sampleRate = sampleRate + } } + + /// Increment a Counter metric aggregation. + case increment(String, by: Double, cardinalities: Cardinalities) + /// Record value of a Gauge metric aggregation. + case record(String, value: Double, cardinalities: Cardinalities) + /// Log a metric event (no aggregation applied + case report(MetricTelemetry.Event) } /// Describes the type of the usage telemetry events supported by the SDK. -public struct UsageTelemetry: SampledTelemetry { +public struct UsageTelemetry { /// Supported usage telemetry events. public enum Event { /// setTrackingConsent API @@ -188,7 +200,9 @@ public enum TelemetryMessage { case error(id: String, message: String, kind: String, stack: String) /// A configuration telemetry. case configuration(ConfigurationTelemetry) + /// A metric telemetry. case metric(MetricTelemetry) + /// A usage telemetry. case usage(UsageTelemetry) } @@ -254,9 +268,9 @@ public extension Telemetry { } let executionTime = -metric.startTime.timeIntervalSinceNow.toInt64Nanoseconds - send( - telemetry: .metric( - MetricTelemetry( + self.metric( + .report( + .init( name: MethodCalledMetric.name, attributes: [ MethodCalledMetric.executionTime: executionTime, @@ -375,6 +389,90 @@ extension Telemetry { self.error(message, error: DDError(error: error), file: file, line: line) } + /// Increments a counter metric by a specified value. + /// + /// This method creates a counter metric that can be used to track the number of times an event occurs. + /// The metric will be incremented by the specified value each time this method is called. + /// + /// - Parameters: + /// - metric: The name of the metric to increment. + /// - value: The amount to increment the metric by. Defaults to 1. + /// - cardinalities: The dimensions along which the metric will be aggregated. + public func increment(metric: String, by value: Double = 1, cardinalities: MetricTelemetry.Cardinalities) { + // swiftlint:disable:previous function_default_parameter_at_end + self.metric(.increment(metric, by: value, cardinalities: cardinalities)) + } + + /// Increments a counter metric by a specified integer value. + /// + /// This method creates a counter metric that can be used to track the number of times an event occurs. + /// The metric will be incremented by the specified integer value each time this method is called. + /// + /// - Parameters: + /// - metric: The name of the metric to increment. + /// - value: The integer amount to increment the metric by. + /// - cardinalities: The dimensions along which the metric will be aggregated. + public func increment(metric: String, by value: Int, cardinalities: MetricTelemetry.Cardinalities) { + self.metric(.increment(metric, by: Double(value), cardinalities: cardinalities)) + } + + /// Records a gauge metric with a specified value. + /// + /// This method creates a gauge metric that can be used to track a value that can go up and down. + /// The metric will be recorded with the specified value each time this method is called. + /// + /// - Parameters: + /// - metric: The name of the metric to record. + /// - value: The value to record for the metric. + /// - cardinalities: The dimensions along which the metric will be aggregated. + public func record(metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities) { + self.metric(.record(metric, value: value, cardinalities: cardinalities)) + } + + /// Records a gauge metric with a specified integer value. + /// + /// This method creates a gauge metric that can be used to track a value that can go up and down. + /// The metric will be recorded with the specified integer value each time this method is called. + /// + /// - Parameters: + /// - metric: The name of the metric to record. + /// - value: The integer value to record for the metric. + /// - cardinalities: The dimensions along which the metric will be aggregated. + public func record(metric: String, value: Int, cardinalities: MetricTelemetry.Cardinalities) { + self.metric(.record(metric, value: Double(value), cardinalities: cardinalities)) + } + + /// Sends a metric telemetry message to the Datadog backend. + /// + /// This method is used to send various types of metric telemetry data, including: + /// - Counter metrics (increments) + /// - Gauge metrics (recorded values) + /// - Custom metric events + /// + /// - Parameter metric: The metric telemetry data to send. + public func metric(_ metric: MetricTelemetry) { + send(telemetry: .metric(metric)) + } + + /// Collects a metric value. + /// + /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicate filtering and + /// are sampled at a different rate. Metric attributes are used to create facets for later querying and graphing. + /// + /// - Parameters: + /// - name: The name of the metric. + /// - attributes: The attributes associated with this metric. + /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate (15% by default). + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public func metric(name: String, attributes: [String: Encodable], sampleRate: SampleRate = MetricTelemetry.defaultSampleRate) { + self.metric(.report(.init(name: name, attributes: attributes, sampleRate: sampleRate))) + } + /// Report a Configuration Telemetry. /// /// The configuration can be partial, the telemetry supports accumulation of @@ -494,25 +592,6 @@ extension Telemetry { useWorkerUrl: useWorkerUrl )) } - - /// Collects a metric value. - /// - /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicate filtering and - /// are sampled at a different rate. Metric attributes are used to create facets for later querying and graphing. - /// - /// - Parameters: - /// - name: The name of the metric. - /// - attributes: The attributes associated with this metric. - /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate (15% by default). - /// Must be a value between `0` (reject all) and `100` (keep all). - /// - /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) - /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. - /// - /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). - public func metric(name: String, attributes: [String: Encodable], sampleRate: SampleRate = MetricTelemetry.defaultSampleRate) { - send(telemetry: .metric(MetricTelemetry(name: name, attributes: attributes, sampleRate: sampleRate))) - } } public struct NOPTelemetry: Telemetry { @@ -555,12 +634,6 @@ extension DatadogCoreProtocol { public var telemetry: Telemetry { CoreTelemetry(core: self) } } -extension DatadogCoreProtocol { - /// Provides access to the `Storage` associated with the core. - /// - Returns: The `Storage` instance. - public var storage: Storage { CoreStorage(core: self) } -} - extension ConfigurationTelemetry { public func merged(with other: Self) -> Self { .init( @@ -622,3 +695,15 @@ extension ConfigurationTelemetry { ) } } + +extension MetricTelemetry.Cardinality: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .string(value): + try container.encode(value) + case let .array(value): + try container.encode(value) + } + } +} diff --git a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift index 4b3a45a20a..5072cbf97f 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift @@ -125,7 +125,7 @@ class TelemetryTests: XCTestCase { telemetry.metric(name: "metric name", attributes: ["attribute": "value"], sampleRate: 4.21) // Then - let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) + let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetricReport }).first) XCTAssertEqual(metric.name, "metric name") XCTAssertEqual(metric.attributes as? [String: String], ["attribute": "value"]) XCTAssertEqual(metric.sampleRate, 4.21) @@ -136,7 +136,7 @@ class TelemetryTests: XCTestCase { telemetry.metric(name: "metric name", attributes: [:]) // Then - let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) + let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetricReport }).first) XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) } @@ -149,7 +149,7 @@ class TelemetryTests: XCTestCase { let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny()) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: MethodCalledMetric.name)) XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) } @@ -157,7 +157,7 @@ class TelemetryTests: XCTestCase { let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny(), tailSampleRate: 42.5) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: MethodCalledMetric.name)) XCTAssertEqual(metric.sampleRate, 42.5) } @@ -172,7 +172,7 @@ class TelemetryTests: XCTestCase { telemetry.stopMethodCalled(metricTrace, isSuccessful: isSuccessful) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: MethodCalledMetric.name)) XCTAssertEqual(metric.attributes[SDKMetricFields.typeKey] as? String, MethodCalledMetric.typeValue) XCTAssertEqual(metric.attributes[SDKMetricFields.headSampleRate] as? SampleRate, 100) XCTAssertEqual(metric.attributes[MethodCalledMetric.operationName] as? String, operationName) @@ -200,10 +200,10 @@ class TelemetryTests: XCTestCase { XCTAssertEqual(receiver.messages.lastTelemetry?.asConfiguration?.batchSize, 123) core.telemetry.metric(name: "metric name", attributes: [:], sampleRate: 15) - XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, "metric name") + XCTAssertEqual(receiver.messages.lastTelemetry?.asMetricReport?.name, "metric name") let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) core.telemetry.stopMethodCalled(metricTrace) - XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, MethodCalledMetric.name) + XCTAssertEqual(receiver.messages.lastTelemetry?.asMetricReport?.name, MethodCalledMetric.name) } } diff --git a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift index acc7a91989..8c99bb52a6 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift @@ -20,7 +20,7 @@ internal struct TelemetryInterceptor: FeatureMessageReceiver { switch telemetry { case .error(let id, let message, let kind, let stack): interceptError(id: id, message: message, kind: kind, stack: stack) - case .metric(let metric) where metric.name == UploadCycleMetric.name: + case let .metric(.report(metric)) where metric.name == UploadCycleMetric.name: // Intercept the 'upload_cycle' metric for aggregation in the rse // metric interceptUploadCycleMetric(attributes: metric.attributes) diff --git a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift index b4257bfff2..4e722641d8 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryReceiver.swift @@ -20,6 +20,9 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { /// Additional sampler for configuration telemetry events, applied in addition to the `sampler`. let configurationExtraSampler: Sampler + // Metric telemetry aggregator + let metricAggregator: MetricTelemetryAggregator + /// Keeps track of current session @ReadWriteLock private var currentSessionID: String? @@ -33,22 +36,35 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { private var eventsCount: Int = 0 /// Creates a RUM Telemetry instance. - /// + /// /// - Parameters: /// - featureScope: RUM feature scope. /// - dateProvider: Current device time provider. /// - sampler: Telemetry events sampler. /// - configurationExtraSampler: Extra sampler for configuration events (applied on top of `sampler`). + /// - metricAggregator: The aggregation used for metrics telemetry + /// - notificationCenter: The Notification center to observe. init( featureScope: FeatureScope, dateProvider: DateProvider, sampler: Sampler, - configurationExtraSampler: Sampler + configurationExtraSampler: Sampler, + metricAggregator: MetricTelemetryAggregator = MetricTelemetryAggregator(), + notificationCenter: NotificationCenter = .default ) { self.featureScope = featureScope self.dateProvider = dateProvider self.sampler = sampler self.configurationExtraSampler = configurationExtraSampler + self.metricAggregator = metricAggregator + + // observe application entering background + notificationCenter.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: ApplicationNotifications.didEnterBackground, + object: nil + ) } /// Receives a message from the bus. @@ -79,23 +95,23 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { error(id: id, message: message, kind: kind, stack: stack) case .configuration(let configuration): send(configuration: configuration) - case let .metric(metric): - if sampled(event: metric) { + case let .metric(.report(metric)): + if Sampler(samplingRate: metric.sampleRate).sample() { send(metric: metric) } case .usage(let usage): - if sampled(event: usage) { + if Sampler(samplingRate: usage.sampleRate).sample() { send(usage: usage) } + case let .metric(.increment(metric, by: value, cardinalities)): + metricAggregator.increment(metric, by: value, cardinalities: cardinalities) + case let .metric(.record(metric, value, cardinalities)): + metricAggregator.record(metric, value: value, cardinalities: cardinalities) } return true } - private func sampled(event: SampledTelemetry) -> Bool { - return Sampler(samplingRate: event.sampleRate).sample() - } - /// Sends a `TelemetryDebugEvent` event. /// see. https://github.com/DataDog/rum-events-format/blob/master/schemas/telemetry/debug-schema.json /// @@ -248,7 +264,48 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { } } - private func send(metric: MetricTelemetry) { + private func send(metric: String, attributes: [String: Encodable], sampleRate: SampleRate) { + let date = dateProvider.now + + record(event: nil) { context, writer in + let rum = context.additionalContext(ofType: RUMCoreContext.self) + + // Override sessionID using standard `SDKMetricFields`, otherwise use current RUM session ID: + var attributes = attributes + let sessionIDOverride: String? = attributes.removeValue(forKey: SDKMetricFields.sessionIDOverrideKey)?.dd.decode() + let sessionID = sessionIDOverride ?? rum?.sessionID + + // Calculates the composition of sample rates. The metric can have up to 3 layers of sampling. + var effectiveSampleRate = sampleRate.composed(with: self.sampler.samplingRate) + if let headSampleRate = attributes.removeValue(forKey: SDKMetricFields.headSampleRate) as? SampleRate { + effectiveSampleRate = effectiveSampleRate.composed(with: headSampleRate) + } + + let event = TelemetryDebugEvent( + dd: .init(), + action: rum?.userActionID.map { .init(id: $0) }, + application: rum.map { .init(id: $0.applicationID) }, + date: date.addingTimeInterval(context.serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds, + effectiveSampleRate: Double(effectiveSampleRate), + experimentalFeatures: nil, + service: "dd-sdk-ios", + session: sessionID.map { .init(id: $0) }, + source: .init(rawValue: context.source) ?? .ios, + telemetry: .init( + device: .init(context.device), + message: "[Mobile Metric] \(metric)", + os: .init(context.device), + telemetryInfo: attributes + ), + version: context.sdkVersion, + view: rum?.viewID.map { .init(id: $0) } + ) + + writer.write(value: event) + } + } + + private func send(metric: MetricTelemetry.Event) { let date = dateProvider.now record(event: nil) { context, writer in @@ -317,6 +374,15 @@ internal final class TelemetryReceiver: FeatureMessageReceiver { } } } + + @objc + private func applicationDidEnterBackground() { + // Report aggregated telemetry metrics when + // the application enters background. + for metric in metricAggregator.flush() { + send(metric: metric) + } + } } private extension TelemetryUsageEvent.Telemetry.Usage { diff --git a/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift b/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift new file mode 100644 index 0000000000..0945cdd0d7 --- /dev/null +++ b/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift @@ -0,0 +1,49 @@ +/* + * 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-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +internal final class MetricTelemetryAggregator { + private struct AggregationKey: Hashable { + let metric: String + let cardinalities: MetricTelemetry.Cardinalities + } + + let sampleRate: SampleRate + + @ReadWriteLock + private var aggregations: [AggregationKey: Double] = [:] + + init(sampleRate: SampleRate = .maxSampleRate) { + self.sampleRate = sampleRate + } + + func increment(_ metric: String, by value: Double, cardinalities: MetricTelemetry.Cardinalities) { + _aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] += value } + } + + func record(_ metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities) { + _aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] = value } + } + + func flush() -> [MetricTelemetry.Event] { + _aggregations.mutate { counters in + defer { counters = [:] } + + return counters.map { key, value in + var attributes: [String: Encodable] = key.cardinalities + attributes[SDKMetricFields.typeKey] = key.metric + attributes[SDKMetricFields.valueKey] = value + return MetricTelemetry.Event( + name: key.metric, + attributes: attributes, + sampleRate: sampleRate + ) + } + } + } +} diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 8f11a2033f..2231f89c66 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -27,7 +27,7 @@ class TelemetryInterceptorTests: XCTestCase { // Then metricController.endMetric(sessionID: sessionID, with: .mockRandom()) - let metric = try XCTUnwrap(telemetry.messages.lastMetric(named: SessionEndedMetric.Constants.name)) + let metric = try XCTUnwrap(telemetry.messages.lastMetricReport(named: SessionEndedMetric.Constants.name)) let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) XCTAssertEqual(rse.sdkErrorsCount.total, 1) } @@ -41,7 +41,7 @@ class TelemetryInterceptorTests: XCTestCase { // When metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny(), tracksBackgroundEvents: .mockRandom()) - let metricTelemetry: TelemetryMessage = .metric(MetricTelemetry(name: UploadCycleMetric.name, attributes: [UploadCycleMetric.track: "feature"], sampleRate: .mockRandom())) + let metricTelemetry: TelemetryMessage = .metric(.report(.init(name: UploadCycleMetric.name, attributes: [UploadCycleMetric.track: "feature"], sampleRate: .mockRandom()))) let result = interceptor.receive(message: .telemetry(metricTelemetry), from: NOPDatadogCore()) XCTAssertTrue(result) diff --git a/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift b/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift new file mode 100644 index 0000000000..1ae284fd4c --- /dev/null +++ b/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift @@ -0,0 +1,104 @@ +/* + * 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-Present Datadog, Inc. + */ + +import XCTest +import DatadogInternal +import TestUtilities +@testable import DatadogRUM + +class MetricTelemetryAggregatorTests: XCTestCase { + func testCounterIncrement() throws { + // Given + let aggregator = MetricTelemetryAggregator() + + let metric1: String = .mockRandom() + + let cardinalities1: MetricTelemetry.Cardinalities = [ + .mockRandom(): .string(.mockRandom()) + ] + + let metric2: String = .mockRandom() + let cardinalities2: MetricTelemetry.Cardinalities = [ + .mockRandom(): .integer(.mockRandom()) + ] + + let iterations: Int = .mockRandom(min: 0, max: 100) + + // When + for _ in 0.. SpanEvent /// The sampling rate for spans created with the default tracer. diff --git a/TestUtilities/Sources/Mocks/DatadogInternal/TelemetryMocks.swift b/TestUtilities/Sources/Mocks/DatadogInternal/TelemetryMocks.swift index a7ea4dd35e..ffd3b964de 100644 --- a/TestUtilities/Sources/Mocks/DatadogInternal/TelemetryMocks.swift +++ b/TestUtilities/Sources/Mocks/DatadogInternal/TelemetryMocks.swift @@ -71,28 +71,58 @@ public class TelemetryMock: Telemetry, CustomStringConvertible { description.append("\n - [error] \(message), kind: \(kind), stack: \(stack)") case .configuration(let configuration): description.append("\n- [configuration] \(configuration)") - case let .metric(metric): + case let .metric(.report(metric)): let attributesString = metric.attributes.map({ "\($0.key): \($0.value)" }).joined(separator: ", ") description.append("\n- [metric] '\(metric.name)' (" + attributesString + ")") case .usage(let usage): description.append("\n- [usage] \(usage)") + case let .metric(.increment(metric, by: value, attributes)): + let attributesString = attributes.map({ "\($0.key): \($0.value)" }).joined(separator: ", ") + description.append("\n- [metric] '\(metric)' + \(value) (" + attributesString + ")") + case let .metric(.record(metric, value, attributes)): + let attributesString = attributes.map({ "\($0.key): \($0.value)" }).joined(separator: ", ") + description.append("\n- [metric] '\(metric)' = \(value) (" + attributesString + ")") } } } public extension Array where Element == TelemetryMessage { /// Returns properties of the first metric message of given name. - func firstMetric(named metricName: String) -> MetricTelemetry? { - return compactMap({ $0.asMetric }) + func firstMetricReport(named metricName: String) -> MetricTelemetry.Event? { + return compactMap({ $0.asMetricReport }) .first(where: { $0.name == metricName }) } /// Returns properties of the first metric message of given name. - func lastMetric(named metricName: String) -> MetricTelemetry? { - return compactMap({ $0.asMetric }) + func lastMetricReport(named metricName: String) -> MetricTelemetry.Event? { + return compactMap({ $0.asMetricReport }) .last(where: { $0.name == metricName }) } + /// Returns properties of the first metric message of given name. + func firstMetricIncrement(named metricName: String) -> (metric: String, increment: Double, cardinalities: MetricTelemetry.Cardinalities)? { + return compactMap({ $0.asMetricIncrement }) + .first(where: { $0.metric == metricName }) + } + + /// Returns properties of the first metric message of given name. + func lastMetricIncrement(named metricName: String) -> (metric: String, increment: Double, cardinalities: MetricTelemetry.Cardinalities)? { + return compactMap({ $0.asMetricIncrement }) + .last(where: { $0.metric == metricName }) + } + + /// Returns properties of the first metric message of given name. + func firstMetricRecord(named metricName: String) -> (metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities)? { + return compactMap({ $0.asMetricRecord }) + .first(where: { $0.metric == metricName }) + } + + /// Returns properties of the first metric message of given name. + func lastMetricRecord(named metricName: String) -> (metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities)? { + return compactMap({ $0.asMetricRecord }) + .last(where: { $0.metric == metricName }) + } + /// Returns attributes of the first debug telemetry in this array. func firstDebug() -> (id: String, message: String, attributes: [String: Encodable]?)? { return compactMap { $0.asDebug }.first @@ -139,14 +169,30 @@ public extension TelemetryMessage { return configuration } - /// Extracts metric attributes if this is metric message. - var asMetric: MetricTelemetry? { - guard case let .metric(metric) = self else { + /// Extracts metric report if this is metric message. + var asMetricReport: MetricTelemetry.Event? { + guard case let .metric(.report(metric)) = self else { return nil } return metric } + /// Extracts metric increment if this is metric message. + var asMetricIncrement: (metric: String, increment: Double, cardinalities: MetricTelemetry.Cardinalities)? { + guard case let .metric(.increment(name, value, cardinalities)) = self else { + return nil + } + return (name, value, cardinalities) + } + + /// Extracts metric record if this is metric message. + var asMetricRecord: (metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities)? { + guard case let .metric(.record(name, value, cardinalities)) = self else { + return nil + } + return (name, value, cardinalities) + } + var asUsage: UsageTelemetry? { guard case let .usage(usage) = self else { return nil From 23328063a23f1ca62fd070a7528cfe68cd8ce7a7 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Fri, 18 Apr 2025 11:02:25 +0200 Subject: [PATCH 5/8] RUM-8042 Remove BatchBlockedMetricAggregator --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 --- DatadogCore/Sources/Core/DatadogCore.swift | 28 +----- .../Core/Upload/DataUploadWorker.swift | 48 ++++----- .../Sources/Core/Upload/FeatureUpload.swift | 6 +- DatadogCore/Sources/Datadog.swift | 3 +- .../BatchBlockedMetricAggregator.swift | 73 -------------- .../Sources/SDKMetrics/BatchMetrics.swift | 5 - .../Core/Upload/DataUploadWorkerTests.swift | 45 +++++---- .../BatchBlockedMetricAggregatorTests.swift | 97 ------------------- .../MetricTelemetryAggregatorTests.swift | 4 +- .../SessionEndedMetricControllerTests.swift | 4 +- 11 files changed, 56 insertions(+), 269 deletions(-) delete mode 100644 DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift delete mode 100644 DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index f1d34ce399..e6004f47fa 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1489,10 +1489,6 @@ D2A7A9002BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9022BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9032BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; - D2AB80BD2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */; }; - D2AB80BE2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */; }; - D2AB80DF2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */; }; - D2AB80E02D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */; }; D2AD1CC32CE4AE6600106C74 /* Color+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */; }; D2AD1CC42CE4AE6600106C74 /* DisplayList+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBD2CE4AE6600106C74 /* DisplayList+Reflection.swift */; }; D2AD1CC52CE4AE6600106C74 /* DisplayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */; }; @@ -3259,8 +3255,6 @@ D2A7840129A534F9003B03BB /* DatadogLogsTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogLogsTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; - D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchBlockedMetricAggregator.swift; sourceTree = ""; }; - D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchBlockedMetricAggregatorTests.swift; sourceTree = ""; }; D2AD1CB92CE4AE6600106C74 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Reflection.swift"; sourceTree = ""; }; D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayList.swift; sourceTree = ""; }; @@ -5408,7 +5402,6 @@ isa = PBXGroup; children = ( 614396712A67D74F00197326 /* BatchMetrics.swift */, - D2AB80BC2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift */, D2E6E8FA2D8039B200FF1398 /* BenchmarkURLSessionTaskDelegate.swift */, ); path = SDKMetrics; @@ -5418,7 +5411,6 @@ isa = PBXGroup; children = ( 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */, - D2AB80DE2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -8427,7 +8419,6 @@ D29CDD3228211A2200F7DAA5 /* TLVBlock.swift in Sources */, 6128F5742BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, D2553829288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */, - D2AB80BE2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */, D224430629E95C2C00274EC7 /* MessageBus.swift in Sources */, 61F930BE2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, 6128F5772BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, @@ -8547,7 +8538,6 @@ 61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */, 61133C612423990D00786299 /* URLSessionClientTests.swift in Sources */, 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, - D2AB80DF2D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */, D22743DB29DEB8B4001A7EF9 /* VitalCPUReaderTests.swift in Sources */, 61DA8CAC2861C3720074A606 /* DirectoriesTests.swift in Sources */, 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */, @@ -9814,7 +9804,6 @@ 6128F5752BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, D2303A0B298D5412001A1FA3 /* AsyncWriter.swift in Sources */, D224430729E95C2E00274EC7 /* MessageBus.swift in Sources */, - D2AB80BD2D91AA0800B4A7FC /* BatchBlockedMetricAggregator.swift in Sources */, 61F930BF2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, 6128F5782BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, D2CB6E3627C50EAE00A62B57 /* ObjcAppLaunchHandler.m in Sources */, @@ -9878,7 +9867,6 @@ 61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */, D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */, - D2AB80E02D931C0800B4A7FC /* BatchBlockedMetricAggregatorTests.swift in Sources */, 61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */, D2DC4BBD27F234E000E4FB96 /* CITestIntegrationTests.swift in Sources */, D2CB6EE427C520D400A62B57 /* FeatureTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index f46188cc55..5200046588 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -75,10 +75,6 @@ internal final class DatadogCore { /// Maximum number of batches per upload. internal let maxBatchesPerUpload: Int - /// Instance to aggregate batch-blocked metric to be sent when the - /// application goes to background. - private let batchBlockedMetricAggregator = BatchBlockedMetricAggregator() - /// Creates a core instance. /// /// - Parameters: @@ -93,7 +89,6 @@ internal final class DatadogCore { /// - maxBatchesPerUpload: Number of batch to process during an upload cycle. /// - backgroundTasksEnabled: Enables upload background task. /// - isRunFromExtension: Set `true` when the SDK is initialised from an extension. - /// - notificationCenter: The Notification center to observe. init( directory: CoreDirectory, dateProvider: DateProvider, @@ -105,8 +100,7 @@ internal final class DatadogCore { applicationVersion: String, maxBatchesPerUpload: Int, backgroundTasksEnabled: Bool, - isRunFromExtension: Bool = false, - notificationCenter: NotificationCenter = .default + isRunFromExtension: Bool = false ) { self.directory = directory self.dateProvider = dateProvider @@ -132,14 +126,6 @@ internal final class DatadogCore { self.contextProvider.publish { [weak self] context in self?.send(message: .context(context)) } - - // observe application entering background - notificationCenter.addObserver( - self, - selector: #selector(applicationDidEnterBackground), - name: ApplicationNotifications.didEnterBackground, - object: nil - ) } /// Sets current user information. @@ -326,15 +312,6 @@ internal final class DatadogCore { stores = [:] features = [:] } - - @objc - private func applicationDidEnterBackground() { - // Report aggregated 'Batch Blocked' telemetry metric - // when the application enters background. - for event in batchBlockedMetricAggregator.flush() { - telemetry.metric(.report(event)) - } - } } extension DatadogCore: DatadogCoreProtocol { @@ -378,8 +355,7 @@ extension DatadogCore: DatadogCoreProtocol { performance: performancePreset, backgroundTasksEnabled: backgroundTasksEnabled, isRunFromExtension: isRunFromExtension, - telemetry: telemetry, - batchBlockedMetricAggregator: batchBlockedMetricAggregator + telemetry: telemetry ) stores[T.name] = ( diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index c0c92cba96..9877a5bb3f 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -44,8 +44,6 @@ internal class DataUploadWorker: DataUploadWorkerType { private var previousUploadStatus: DataUploadStatus? - private let batchBlockedMetricAggregator: BatchBlockedMetricAggregator? - init( queue: DispatchQueue, fileReader: Reader, @@ -56,8 +54,7 @@ internal class DataUploadWorker: DataUploadWorkerType { featureName: String, telemetry: Telemetry, maxBatchesPerUpload: Int, - backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil, - batchBlockedMetricAggregator: BatchBlockedMetricAggregator? = nil + backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil ) { self.queue = queue self.fileReader = fileReader @@ -68,7 +65,6 @@ internal class DataUploadWorker: DataUploadWorkerType { self.delay = delay self.featureName = featureName self.telemetry = telemetry - self.batchBlockedMetricAggregator = batchBlockedMetricAggregator let readWorkItem = DispatchWorkItem { [weak self] in guard let self = self else { @@ -250,20 +246,23 @@ internal class DataUploadWorker: DataUploadWorkerType { } private func sendBatchBlockedMetric(blockers: [DataUploadConditions.Blocker], batchCount: Int) { - guard !blockers.isEmpty else { + guard batchCount > 0, !blockers.isEmpty else { return } - batchBlockedMetricAggregator?.increment( + telemetry.increment( + metric: BatchBlockedMetric.typeValue, by: batchCount, - track: featureName, - blockers: blockers.map { - switch $0 { - case .battery: return "low_battery" - case .lowPowerModeOn: return "lpm" - case .networkReachability: return "offline" - } - } + cardinalities: [ + BatchMetric.trackKey: .string(featureName), + BatchBlockedMetric.blockers: .array(blockers.map { + switch $0 { + case .battery: return .string("low_battery") + case .lowPowerModeOn: return .string("lpm") + case .networkReachability: return .string("offline") + } + }) + ] ) } @@ -272,15 +271,18 @@ internal class DataUploadWorker: DataUploadWorkerType { return } - batchBlockedMetricAggregator?.increment( + telemetry.increment( + metric: BatchBlockedMetric.typeValue, by: batchCount, - track: featureName, - failure: { - switch error { - case let .httpError(code): return "intake-code-\(code.rawValue)" - case let .networkError(error): return "network-code-\(error.code)" - } - }() + cardinalities: [ + BatchMetric.trackKey: .string(featureName), + BatchBlockedMetric.failure: .string({ + switch error { + case let .httpError(code): return "intake-code-\(code.rawValue)" + case let .networkError(error): return "network-code-\(error.code)" + } + }()) + ] ) } } diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift index 7071ac18e9..401cb0a001 100644 --- a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift @@ -20,8 +20,7 @@ internal struct FeatureUpload { performance: PerformancePreset, backgroundTasksEnabled: Bool, isRunFromExtension: Bool, - telemetry: Telemetry, - batchBlockedMetricAggregator: BatchBlockedMetricAggregator? = nil + telemetry: Telemetry ) { let uploadQueue = DispatchQueue( label: "com.datadoghq.ios-sdk-\(featureName)-upload", @@ -64,8 +63,7 @@ internal struct FeatureUpload { featureName: featureName, telemetry: telemetry, maxBatchesPerUpload: performance.maxBatchesPerUpload, - backgroundTaskCoordinator: backgroundTaskCoordinator, - batchBlockedMetricAggregator: batchBlockedMetricAggregator + backgroundTaskCoordinator: backgroundTaskCoordinator ) ) } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 25cded3786..23f43cc700 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -661,8 +661,7 @@ extension DatadogCore { applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, backgroundTasksEnabled: configuration.backgroundTasksEnabled, - isRunFromExtension: isRunFromExtension, - notificationCenter: configuration.notificationCenter + isRunFromExtension: isRunFromExtension ) telemetry.configuration( diff --git a/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift b/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift deleted file mode 100644 index 29d53da54a..0000000000 --- a/DatadogCore/Sources/SDKMetrics/BatchBlockedMetricAggregator.swift +++ /dev/null @@ -1,73 +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-Present Datadog, Inc. - */ - -import Foundation -import DatadogInternal - -internal final class BatchBlockedMetricAggregator { - private struct AggregationKey: Hashable { - let track: String - let failure: String? - let blockers: [String]? - } - - let sampleRate: SampleRate - - @ReadWriteLock - private var aggregations: [AggregationKey: Int] = [:] - - init(sampleRate: SampleRate = MetricTelemetry.defaultSampleRate) { - self.sampleRate = sampleRate - } - - func increment(by count: Int, track: String, failure: String) { - increment(by: count, key: AggregationKey(track: track, failure: failure, blockers: nil)) - } - - func increment(by count: Int, track: String, blockers: [String]) { - increment(by: count, key: AggregationKey(track: track, failure: nil, blockers: blockers)) - } - - private func increment(by count: Int, key: AggregationKey) { - _aggregations.mutate { $0[key, default: 0] += count } - } - - func flush() -> [MetricTelemetry.Event] { - _aggregations.mutate { aggregations in - defer { aggregations = [:] } - - return aggregations.compactMap { key, value in - if let failure = key.failure { - return MetricTelemetry.Event( - name: BatchBlockedMetric.name, - attributes: [ - SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, - BatchMetric.trackKey: key.track, - BatchBlockedMetric.batchCount: value, - BatchBlockedMetric.failure: failure - ], - sampleRate: sampleRate - ) - } - - if let blockers = key.blockers { - return MetricTelemetry.Event( - name: BatchBlockedMetric.name, - attributes: [ - SDKMetricFields.typeKey: BatchBlockedMetric.typeValue, - BatchMetric.trackKey: key.track, - BatchBlockedMetric.batchCount: value, - BatchBlockedMetric.blockers: blockers - ], - sampleRate: sampleRate - ) - } - - return nil - } - } - } -} diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index a7ae907887..2006b9a151 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -133,13 +133,8 @@ internal enum BatchClosedMetric { /// Definition of "Batch Blocked" telemetry. internal enum BatchBlockedMetric { - /// The name of this metric, included in telemetry log. - /// Note: the "[Mobile Metric]" prefix is added when sending this telemetry in RUM. - static let name = "Batch Blocked" /// Metric type value. static let typeValue = "batch blocked" - /// The key for count of bacthes being blocked. - static let batchCount = "count" /// List of upload blocker reasons static let blockers = "blockers" /// The blocking failure reason. diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index f2b1dc4fed..75cd94a3a5 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -89,7 +89,7 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 0) XCTAssertEqual(telemetry.messages.count, 3) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -140,7 +140,7 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 1) XCTAssertEqual(telemetry.messages.count, 1) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, featureName) } @@ -522,9 +522,10 @@ class DataUploadWorkerTests: XCTestCase { ) } - func testWhenUploadIsBlocked_itDoesSendBatchBlockedTelemetry() throws { + func testWhenUploadIsBlocked_itIncrementsBatchBlockedTelemetry() throws { // Given - let aggregator = BatchBlockedMetricAggregator() + let telemetry = TelemetryMock() + writer.write(value: ["key": "value"]) // When let uploadExpectation = self.expectation(description: "Upload has started") @@ -553,19 +554,18 @@ class DataUploadWorkerTests: XCTestCase { uploadConditions: .neverUpload(), delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), featureName: featureName, - telemetry: NOPTelemetry(), - maxBatchesPerUpload: .mockRandom(min: 1, max: 100), - batchBlockedMetricAggregator: aggregator + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) ) wait(for: [uploadExpectation], timeout: 0.5) worker.cancelSynchronously() // Then - let metrics = aggregator.flush() - XCTAssertEqual(metrics.count, 1) - XCTAssertEqual(metrics.first?.attributes["blockers"] as? [String], ["offline", "low_battery"]) - XCTAssertEqual(metrics.first?.attributes["track"] as? String, featureName) + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: BatchBlockedMetric.typeValue)) + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["blockers"], .array([.string("offline"), .string("low_battery")])) + XCTAssertEqual(metric.cardinalities["track"], .string(featureName)) } func testWhenDataIsUploadedWithServerError_itDoesNotSendErrorTelemetry() throws { @@ -610,7 +610,7 @@ class DataUploadWorkerTests: XCTestCase { // Then XCTAssertEqual(telemetry.messages.count, 1) - XCTAssertNotNil(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + XCTAssertNotNil(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") } func testWhenDataIsUploadedWithAlertingStatusCode_itSendsErrorTelemetry() throws { @@ -698,11 +698,11 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(error.message, #"Data upload finished with error - Error Domain=abc Code=0 "(null)""#) } - func testWhenDataIsUploadedWithRetryableStatusCode_itSendsBatchBlockedTelemetry() throws { + func testWhenDataIsUploadedWithRetryableStatusCode_itIncrementsBatchBlockedTelemetry() throws { // Given - let aggregator = BatchBlockedMetricAggregator() - + let telemetry = TelemetryMock() writer.write(value: ["key": "value"]) + let randomStatusCode: HTTPResponseStatusCode = [ .requestTimeout, .tooManyRequests, @@ -730,19 +730,18 @@ class DataUploadWorkerTests: XCTestCase { uploadConditions: .alwaysUpload(), delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), featureName: featureName, - telemetry: NOPTelemetry(), - maxBatchesPerUpload: .mockRandom(min: 1, max: 100), - batchBlockedMetricAggregator: aggregator + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) ) wait(for: [startUploadExpectation], timeout: 0.5) worker.cancelSynchronously() // Then - let metrics = aggregator.flush() - XCTAssertEqual(metrics.count, 1) - XCTAssertEqual(metrics.first?.attributes["failure"] as? String, "intake-code-\(randomStatusCode.rawValue)") - XCTAssertEqual(metrics.first?.attributes["track"] as? String, featureName) + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: BatchBlockedMetric.typeValue)) + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["failure"], .string("intake-code-\(randomStatusCode.rawValue)")) + XCTAssertEqual(metric.cardinalities["track"], .string(featureName)) } func testWhenDataCannotBePreparedForUpload_itSendsErrorTelemetry() throws { @@ -781,7 +780,7 @@ class DataUploadWorkerTests: XCTestCase { let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message, #"Failed to initiate 'some-feature' data upload - Failed to prepare upload"#) - let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") XCTAssertEqual(metric.attributes["track"] as? String, "some-feature") } diff --git a/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift b/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift deleted file mode 100644 index 8d9f39f7a3..0000000000 --- a/DatadogCore/Tests/Datadog/SDKMetrics/BatchBlockedMetricAggregatorTests.swift +++ /dev/null @@ -1,97 +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-Present Datadog, Inc. - */ - -import XCTest -import TestUtilities -@testable import DatadogCore - -class BatchBlockedMetricAggregatorTests: XCTestCase { - func testFailureIncrement() throws { - // Given - let aggregator = BatchBlockedMetricAggregator() - - let track1: String = .mockRandom() - let failure1: String = .mockRandom() - - let track2: String = .mockRandom() - let failure2: String = .mockRandom() - - let iterations: Int = .mockRandom(min: 0, max: 100) - - // When - for _ in 0.. Date: Fri, 18 Apr 2025 11:59:48 +0200 Subject: [PATCH 6/8] RUM-8042 Move upload_cycle to dedicated aggregation --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 +++++----- .../Core/Upload/DataUploadWorker.swift | 6 ++--- .../SDKMetrics/UploadCycleMetric.swift | 0 .../Core/Upload/DataUploadWorkerTests.swift | 19 +++++++++------ .../Integrations/TelemetryInterceptor.swift | 10 -------- .../SDKMetrics/SessionEndedMetric.swift | 23 +----------------- .../SessionEndedMetricController.swift | 8 ------- .../TelemetryInterceptorTests.swift | 20 ---------------- .../SessionEndedMetricControllerTests.swift | 1 - .../SDKMetrics/SessionEndedMetricTests.swift | 24 ------------------- 10 files changed, 22 insertions(+), 101 deletions(-) rename {DatadogInternal => DatadogCore}/Sources/SDKMetrics/UploadCycleMetric.swift (100%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index e6004f47fa..b2c7b768fd 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -1095,8 +1095,6 @@ D22743E729DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */; }; D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; - D22789362D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */; }; - D22789372D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */; }; D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; @@ -1150,6 +1148,8 @@ D23354FD2A42E32000AFCAE2 /* InternalExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */; }; D234613128B7713000055D4C /* FeatureContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D234613028B7712F00055D4C /* FeatureContextTests.swift */; }; D234613228B7713000055D4C /* FeatureContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D234613028B7712F00055D4C /* FeatureContextTests.swift */; }; + D2393EF82DB2555F006B3C75 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2393EF72DB2555F006B3C75 /* UploadCycleMetric.swift */; }; + D2393EF92DB2555F006B3C75 /* UploadCycleMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2393EF72DB2555F006B3C75 /* UploadCycleMetric.swift */; }; D23F8E5229DDCD28001CFAE8 /* UIViewControllerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA2251118FB00C816E5 /* UIViewControllerHandler.swift */; }; D23F8E5329DDCD28001CFAE8 /* RUMCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63A24BF1A4B008053F2 /* RUMCommand.swift */; }; D23F8E5429DDCD28001CFAE8 /* ValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611529A425E3DD51004F740E /* ValuePublisher.swift */; }; @@ -3121,7 +3121,6 @@ D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageReceiverTests.swift; sourceTree = ""; }; D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+SessionReplay.swift"; sourceTree = ""; }; D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportReceiverTests.swift; sourceTree = ""; }; - D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadCycleMetric.swift; sourceTree = ""; }; D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convenience.swift"; sourceTree = ""; }; @@ -3166,6 +3165,7 @@ D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalExtended.swift; sourceTree = ""; }; D234613028B7712F00055D4C /* FeatureContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureContextTests.swift; sourceTree = ""; }; D236BE2729520FED00676E67 /* CrashReportReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportReceiver.swift; sourceTree = ""; }; + D2393EF72DB2555F006B3C75 /* UploadCycleMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadCycleMetric.swift; sourceTree = ""; }; D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogRUM.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D23F8ECD29DDCD38001CFAE8 /* DatadogRUMTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogRUMTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D240684D27CE6C9E00C04F44 /* Example tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -5402,6 +5402,7 @@ isa = PBXGroup; children = ( 614396712A67D74F00197326 /* BatchMetrics.swift */, + D2393EF72DB2555F006B3C75 /* UploadCycleMetric.swift */, D2E6E8FA2D8039B200FF1398 /* BenchmarkURLSessionTaskDelegate.swift */, ); path = SDKMetrics; @@ -5420,7 +5421,6 @@ children = ( 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */, A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */, - D22789352D64A0D3007E9DB0 /* UploadCycleMetric.swift */, ); path = SDKMetrics; sourceTree = ""; @@ -8447,6 +8447,7 @@ 617699182A860D9D0030022B /* HTTPClient.swift in Sources */, D21C26C528A3B49C005DD405 /* FeatureStorage.swift in Sources */, 61133BD42423979B00786299 /* FileReader.swift in Sources */, + D2393EF82DB2555F006B3C75 /* UploadCycleMetric.swift in Sources */, D29294E0291D5ED100F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, 61D3E0D9277B23F1008BE766 /* KronosNTPProtocol.swift in Sources */, 61D3E0DA277B23F1008BE766 /* KronosTimeFreeze.swift in Sources */, @@ -8950,7 +8951,6 @@ D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, D24EC3D92DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */, E2AA55E72C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, - D22789372D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */, D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, @@ -9832,6 +9832,7 @@ 617699192A860D9D0030022B /* HTTPClient.swift in Sources */, D2FB1255292E0E99005B13F8 /* TrackingConsentPublisher.swift in Sources */, D26C49C0288982DA00802B2D /* FeatureUpload.swift in Sources */, + D2393EF92DB2555F006B3C75 /* UploadCycleMetric.swift in Sources */, D2CB6E8127C50EAE00A62B57 /* DataUploader.swift in Sources */, D2CB6E8827C50EAE00A62B57 /* FileReader.swift in Sources */, D2CB6E8D27C50EAE00A62B57 /* KronosNTPProtocol.swift in Sources */, @@ -10065,7 +10066,6 @@ D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, D24EC3DA2DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */, E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, - D22789362D64A0D7007E9DB0 /* UploadCycleMetric.swift in Sources */, D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index 9877a5bb3f..4970e8830a 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -239,9 +239,9 @@ internal class DataUploadWorker: DataUploadWorkerType { } private func sendUploadCycleMetric() { - telemetry.metric( - name: UploadCycleMetric.name, - attributes: [UploadCycleMetric.track: featureName] + telemetry.increment( + metric: UploadCycleMetric.name, + cardinalities: [UploadCycleMetric.track: .string(featureName)] ) } diff --git a/DatadogInternal/Sources/SDKMetrics/UploadCycleMetric.swift b/DatadogCore/Sources/SDKMetrics/UploadCycleMetric.swift similarity index 100% rename from DatadogInternal/Sources/SDKMetrics/UploadCycleMetric.swift rename to DatadogCore/Sources/SDKMetrics/UploadCycleMetric.swift diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 75cd94a3a5..ee2ec5f512 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -89,8 +89,9 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 0) XCTAssertEqual(telemetry.messages.count, 3) - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["track"] as? String, featureName) + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["track"], .string(featureName)) } func testItUploadsDataSequentiallyWithoutDelay_whenMaxBatchesPerUploadIsSet() throws { @@ -140,8 +141,9 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 1) XCTAssertEqual(telemetry.messages.count, 1) - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["track"] as? String, featureName) + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["track"], .string(featureName)) } func testGivenDataToUpload_whenUploadFinishesAndDoesNotNeedToBeRetried_thenDataIsDeleted() { @@ -610,7 +612,9 @@ class DataUploadWorkerTests: XCTestCase { // Then XCTAssertEqual(telemetry.messages.count, 1) - XCTAssertNotNil(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["track"], .string(featureName)) } func testWhenDataIsUploadedWithAlertingStatusCode_itSendsErrorTelemetry() throws { @@ -780,8 +784,9 @@ class DataUploadWorkerTests: XCTestCase { let error = try XCTUnwrap(telemetry.messages.firstError(), "An error should be send to `telemetry`.") XCTAssertEqual(error.message, #"Failed to initiate 'some-feature' data upload - Failed to prepare upload"#) - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") - XCTAssertEqual(metric.attributes["track"] as? String, "some-feature") + let metric = try XCTUnwrap(telemetry.messages.firstMetricIncrement(named: "upload_cycle"), "An upload cycle metric should be send to `telemetry`.") + XCTAssertEqual(metric.increment, 1) + XCTAssertEqual(metric.cardinalities["track"], .string("some-feature")) } // MARK: - Tearing Down diff --git a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift index 8c99bb52a6..ec4b7b9185 100644 --- a/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift +++ b/DatadogRUM/Sources/Integrations/TelemetryInterceptor.swift @@ -20,12 +20,6 @@ internal struct TelemetryInterceptor: FeatureMessageReceiver { switch telemetry { case .error(let id, let message, let kind, let stack): interceptError(id: id, message: message, kind: kind, stack: stack) - case let .metric(.report(metric)) where metric.name == UploadCycleMetric.name: - // Intercept the 'upload_cycle' metric for aggregation in the rse - // metric - interceptUploadCycleMetric(attributes: metric.attributes) - return true // do not forward the message - default: break } @@ -36,8 +30,4 @@ internal struct TelemetryInterceptor: FeatureMessageReceiver { private func interceptError(id: String, message: String, kind: String, stack: String) { sessionEndedMetric.track(sdkErrorKind: kind, in: nil) // `nil` - track in current session } - - private func interceptUploadCycleMetric(attributes: [String: Encodable]) { - sessionEndedMetric.track(uploadCycle: attributes, in: nil) // `nil` - track in current session - } } diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index ae10029ddf..02956b8cac 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -95,9 +95,6 @@ internal class SessionEndedMetric { /// Indicates if the session was stopped through `stopSession()` API. private var wasStopped = false - /// Information about the upload cycle during the session. - private var uploadCycle: [String: Int] = [:] - /// If `RUM.Configuration.trackBackgroundEvents` was enabled for this session. private let tracksBackgroundEvents: Bool @@ -199,18 +196,6 @@ internal class SessionEndedMetric { wasStopped = true } - /// Tracks the upload quality metric for aggregation. - /// - /// - Parameters: - /// - attributes: The upload quality attributes - func track(uploadCycle attributes: [String: Encodable]) { - guard let track = attributes[UploadCycleMetric.track] as? String else { - return - } - - uploadCycle[track, default: 0] += 1 - } - // MARK: - Exporting Attributes /// Set of quality and diagnostic attributes for the Session Ended metric. @@ -322,10 +307,6 @@ internal class SessionEndedMetric { /// Information on number of events missed due to absence of an active view. let noViewEventsCount: NoViewEventsCount - /// Information about the upload cycles during the session. - /// The upload cycles is splitting between upload track name. - let uploadCycle: [String: Int] - enum CodingKeys: String, CodingKey { case processType = "process_type" case precondition @@ -337,7 +318,6 @@ internal class SessionEndedMetric { case sdkErrorsCount = "sdk_errors_count" case ntpOffset = "ntp_offset" case noViewEventsCount = "no_view_events_count" - case uploadCycle = "upload_cycle" } } @@ -409,8 +389,7 @@ internal class SessionEndedMetric { resources: missedEvents[.resource] ?? 0, errors: missedEvents[.error] ?? 0, longTasks: missedEvents[.longTask] ?? 0 - ), - uploadCycle: uploadCycle + ) ) ] } diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index 1ad24917e2..ad81a46ea8 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -117,14 +117,6 @@ internal final class SessionEndedMetricController { } } - /// Tracks the upload quality metric for aggregation. - /// - /// - Parameters: - /// - attributes: The upload quality attributes - func track(uploadCycle attributes: [String: Encodable], in sessionID: RUMUUID?) { - updateMetric(for: sessionID) { $0?.track(uploadCycle: attributes) } - } - private func updateMetric(for sessionID: RUMUUID?, _ mutation: (inout SessionEndedMetric?) throws -> Void) { _metricsBySessionID.mutate { metrics in guard let sessionID = (sessionID ?? pendingSessionIDs.last) else { diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 2231f89c66..ff5952cc05 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -31,24 +31,4 @@ class TelemetryInterceptorTests: XCTestCase { let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) XCTAssertEqual(rse.sdkErrorsCount.total, 1) } - - func testWhenInterceptingUploadQualityMetric_itItUpdatesSessionEndedMetric() throws { - let sessionID: RUMUUID = .mockRandom() - - // Given - let metricController = SessionEndedMetricController(telemetry: telemetry, sampleRate: 100) - let interceptor = TelemetryInterceptor(sessionEndedMetric: metricController) - - // When - metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny(), tracksBackgroundEvents: .mockRandom()) - let metricTelemetry: TelemetryMessage = .metric(.report(.init(name: UploadCycleMetric.name, attributes: [UploadCycleMetric.track: "feature"], sampleRate: .mockRandom()))) - let result = interceptor.receive(message: .telemetry(metricTelemetry), from: NOPDatadogCore()) - XCTAssertTrue(result) - - // Then - metricController.endMetric(sessionID: sessionID, with: .mockRandom()) - let metric = try XCTUnwrap(telemetry.messages.lastMetric(named: SessionEndedMetric.Constants.name)) - let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) - XCTAssertEqual(rse.uploadCycle["feature"], 1) - } } diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index 56e47bb66d..2c3e60abb3 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -114,7 +114,6 @@ class SessionEndedMetricControllerTests: XCTestCase { { controller.track(missedEventType: .action, in: sessionIDs.randomElement()!) }, { controller.track(missedEventType: .resource, in: nil) }, { controller.trackWasStopped(sessionID: nil) }, - { controller.track(uploadCycle: mockRandomAttributes(), in: nil) }, { controller.endMetric(sessionID: sessionIDs.randomElement()!, with: .mockRandom()) }, ], iterations: 100 diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index 9eb21b51f0..3614714a65 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -589,28 +589,6 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.noViewEventsCount.longTasks, missedLongTasksCount) } - // MARK: - Tracks Upload Quality - - func testUploadQualityMetricAggregation() throws { - let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSApp)) - - let count1: Int = .mockRandom(min: 10, max: 100) - for _ in 0.. Date: Fri, 18 Apr 2025 14:01:02 +0200 Subject: [PATCH 7/8] RUM-8042 Move pending_batches to dedicated aggregation --- .../Core/Storage/FilesOrchestrator.swift | 48 ++++++++++---- .../Sources/SDKMetrics/BatchMetrics.swift | 6 ++ .../FilesOrchestrator+MetricsTests.swift | 66 ++++++++++++++----- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift index 58f3d1dae2..08c6ab619f 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -60,10 +60,6 @@ internal class FilesOrchestrator: FilesOrchestratorType { /// An extra information to include in metrics or `nil` if metrics should not be reported for this orchestrator. let metricsData: MetricsData? - /// Tracks number of pending batches in the track's directory - @ReadWriteLock - private var pendingBatches: Int = 0 - var trackName: String { metricsData?.trackName ?? "Unknown" } @@ -133,9 +129,8 @@ internal class FilesOrchestrator: FilesOrchestratorType { lastWritableFileObjectsCount = 1 lastWritableFileApproximatedSize = writeSize lastWritableFileLastWriteDate = dateProvider.now - - // Increment pending batches for telemetry - _pendingBatches.mutate { $0 += 1 } + // Increment pending batches in telemetry + incrementPendingBatches() return newFile } @@ -193,7 +188,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { let files = try directory.files() // Reset pending batches for telemetry - pendingBatches = files.count + recordPendingBatches(count: files.count) let filesFromOldest = try files .compactMap { try deleteFileIfItsObsolete(file: $0, fileCreationDate: fileCreationDateFrom(fileName: $0.name)) } @@ -228,7 +223,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { #endif try readableFile.delete() // Decrement pending batches at each batch deletion - _pendingBatches.mutate { $0 -= 1 } + incrementPendingBatches(by: -1) sendBatchDeletedMetric(batchFile: readableFile, deletionReason: deletionReason) } catch { telemetry.error("Failed to delete file", error: error) @@ -259,7 +254,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { let fileWithSize = filesWithSizeSortedByCreationDate.removeFirst() try fileWithSize.file.delete() // Decrement pending batches at each batch deletion - _pendingBatches.mutate { $0 -= 1 } + incrementPendingBatches(by: -1) sendBatchDeletedMetric(batchFile: fileWithSize.file, deletionReason: .purged) sizeFreed += fileWithSize.size } @@ -272,7 +267,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { if fileAge > performance.maxFileAgeForRead { try file.delete() // Decrement pending batches at each batch deletion - _pendingBatches.mutate { $0 -= 1 } + incrementPendingBatches(by: -1) sendBatchDeletedMetric(batchFile: file, deletionReason: .obsolete) return nil } else { @@ -309,13 +304,40 @@ internal class FilesOrchestrator: FilesOrchestratorType { BatchDeletedMetric.batchAgeKey: batchAge.toMilliseconds, BatchDeletedMetric.batchRemovalReasonKey: deletionReason.toString(), BatchDeletedMetric.inBackgroundKey: false, - BatchDeletedMetric.backgroundTasksEnabled: metricsData.backgroundTasksEnabled, - BatchDeletedMetric.pendingBatches: pendingBatches + BatchDeletedMetric.backgroundTasksEnabled: metricsData.backgroundTasksEnabled ], sampleRate: BatchDeletedMetric.sampleRate ) } + private func incrementPendingBatches(by increment: Double = 1) { + guard let metricsData = metricsData else { + return + } + + telemetry.increment( + metric: PendingBatchMetric.typeValue, + by: increment, + cardinalities: [ + BatchMetric.trackKey: .string(metricsData.trackName) + ] + ) + } + + private func recordPendingBatches(count: Int) { + guard let metricsData = metricsData else { + return + } + + telemetry.record( + metric: PendingBatchMetric.typeValue, + value: count, + cardinalities: [ + BatchMetric.trackKey: .string(metricsData.trackName) + ] + ) + } + /// Sends "Batch Closed" telemetry log. /// - Parameters: /// - fileName: The name of the batch that was closed. diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 2006b9a151..0e28601abc 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -140,3 +140,9 @@ internal enum BatchBlockedMetric { /// The blocking failure reason. static let failure = "failure" } + +/// Definition of "Pending Batches" telemetry. +internal enum PendingBatchMetric { + /// Metric type value. + static let typeValue = "pending batches" +} diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 5930611019..7799db48d4 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -46,7 +46,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { // MARK: - "Batch Deleted" Metric - func testWhenReadableFileIsDeleted_itSendsBatchDeletedMetric() throws { + func testWhenReadableFileIsDeleted_itSendsTelemetryMetric() throws { // Given let orchestrator = createOrchestrator() let expectedBatchAge = storage.minFileAgeForRead + 1 @@ -63,8 +63,8 @@ class FilesOrchestrator_MetricsTests: XCTestCase { orchestrator.delete(readableFile: file, deletionReason: .intakeCode(responseCode: 202)) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) - DDAssertJSONEqual(metric.attributes, [ + let batchDeleted = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) + DDAssertJSONEqual(batchDeleted.attributes, [ "metric_type": "batch deleted", "track": "track name", "consent": "consent value", @@ -76,13 +76,20 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "in_background": false, "background_tasks_enabled": false, "batch_age": expectedBatchAge.toMilliseconds, - "batch_removal_reason": "intake-code-202", - "pending_batches": 1 + "batch_removal_reason": "intake-code-202" ]) - XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) + XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate) + + let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in + XCTAssertEqual(metric.metric, "pending batches") + XCTAssertEqual(metric.cardinalities["track"], .string("track name")) + return count + metric.increment + } + + XCTAssertEqual(pendingBatches, 1) } - func testWhenObsoleteFileIsDeleted_itSendsBatchDeletedMetric() throws { + func testWhenObsoleteFileIsDeleted_itSendsTelemetryMetric() throws { // Given: // - request some batch to be created let orchestrator = createOrchestrator() @@ -95,8 +102,8 @@ class FilesOrchestrator_MetricsTests: XCTestCase { _ = orchestrator.getReadableFiles() // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) - DDAssertJSONEqual(metric.attributes, [ + let batchDeleted = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) + DDAssertJSONEqual(batchDeleted.attributes, [ "metric_type": "batch deleted", "track": "track name", "consent": "consent value", @@ -108,13 +115,29 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "in_background": false, "background_tasks_enabled": false, "batch_age": (storage.maxFileAgeForRead + 1).toMilliseconds, - "batch_removal_reason": "obsolete", - "pending_batches": 0 + "batch_removal_reason": "obsolete" ]) - XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) + XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate) + + let pendingBatches = telemetry.messages.reduce(0) { count, message in + switch message { + case let .metric(.record(metric, value, cardinalities)): + XCTAssertEqual(metric, "pending batches") + XCTAssertEqual(cardinalities["track"], .string("track name")) + return value + case let .metric(.increment(metric, value, cardinalities)): + XCTAssertEqual(metric, "pending batches") + XCTAssertEqual(cardinalities["track"], .string("track name")) + return count + value + default: + return count + } + } + + XCTAssertEqual(pendingBatches, 0) } - func testWhenDirectoryIsPurged_itSendsBatchDeletedMetrics() throws { + func testWhenDirectoryIsPurged_itSendsTelemetryMetrics() throws { // Given: some batch // - request batch to be created // - write more data than allowed directory size limit @@ -130,8 +153,8 @@ class FilesOrchestrator_MetricsTests: XCTestCase { _ = try orchestrator.getWritableFile(writeSize: 1) // Then - let metric = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) - DDAssertJSONEqual(metric.attributes, [ + let batchDeleted = try XCTUnwrap(telemetry.messages.firstMetricReport(named: "Batch Deleted")) + DDAssertJSONEqual(batchDeleted.attributes, [ "metric_type": "batch deleted", "track": "track name", "consent": "consent value", @@ -143,10 +166,17 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "in_background": false, "background_tasks_enabled": false, "batch_age": expectedBatchAge.toMilliseconds, - "batch_removal_reason": "purged", - "pending_batches": 0 + "batch_removal_reason": "purged" ]) - XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) + XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate) + + let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in + XCTAssertEqual(metric.metric, "pending batches") + XCTAssertEqual(metric.cardinalities["track"], .string("track name")) + return count + metric.increment + } + + XCTAssertEqual(pendingBatches, 1) } // MARK: - "Batch Closed" Metric From f4df695cc17156980f00d4794121f8b1e04a8482 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Tue, 22 Apr 2025 13:00:31 +0200 Subject: [PATCH 8/8] RUM-8042 Group metrics with same cadinality --- .../Sources/SDKMetrics/BatchMetrics.swift | 4 +- .../FilesOrchestrator+MetricsTests.swift | 8 +- .../MetricTelemetryAggregator.swift | 75 +++++++++++++++---- .../MetricTelemetryAggregatorTests.swift | 26 +++---- 4 files changed, 77 insertions(+), 36 deletions(-) diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 0e28601abc..d008e616bf 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -134,7 +134,7 @@ internal enum BatchClosedMetric { /// Definition of "Batch Blocked" telemetry. internal enum BatchBlockedMetric { /// Metric type value. - static let typeValue = "batch blocked" + static let typeValue = "batch_blocked" /// List of upload blocker reasons static let blockers = "blockers" /// The blocking failure reason. @@ -144,5 +144,5 @@ internal enum BatchBlockedMetric { /// Definition of "Pending Batches" telemetry. internal enum PendingBatchMetric { /// Metric type value. - static let typeValue = "pending batches" + static let typeValue = "pending_batches" } diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 7799db48d4..208f6f95c6 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -81,7 +81,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate) let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in - XCTAssertEqual(metric.metric, "pending batches") + XCTAssertEqual(metric.metric, "pending_batches") XCTAssertEqual(metric.cardinalities["track"], .string("track name")) return count + metric.increment } @@ -122,11 +122,11 @@ class FilesOrchestrator_MetricsTests: XCTestCase { let pendingBatches = telemetry.messages.reduce(0) { count, message in switch message { case let .metric(.record(metric, value, cardinalities)): - XCTAssertEqual(metric, "pending batches") + XCTAssertEqual(metric, "pending_batches") XCTAssertEqual(cardinalities["track"], .string("track name")) return value case let .metric(.increment(metric, value, cardinalities)): - XCTAssertEqual(metric, "pending batches") + XCTAssertEqual(metric, "pending_batches") XCTAssertEqual(cardinalities["track"], .string("track name")) return count + value default: @@ -171,7 +171,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase { XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate) let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in - XCTAssertEqual(metric.metric, "pending batches") + XCTAssertEqual(metric.metric, "pending_batches") XCTAssertEqual(metric.cardinalities["track"], .string("track name")) return count + metric.increment } diff --git a/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift b/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift index 0945cdd0d7..6cd031e09b 100644 --- a/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift +++ b/DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift @@ -7,39 +7,86 @@ import Foundation import DatadogInternal +/// A class that aggregates metric telemetry data before sending it to the Datadog. +/// +/// This aggregator supports two types of metrics: +/// - Counter metrics: Values that can only increase (e.g., number of events) +/// - Gauge metrics: Values that can go up and down (e.g., current memory usage) +/// +/// Metrics can be aggregated along different dimensions using cardinalities, allowing for +/// detailed analysis of metric data across various contexts. internal final class MetricTelemetryAggregator { - private struct AggregationKey: Hashable { - let metric: String - let cardinalities: MetricTelemetry.Cardinalities - } + private typealias AggregationKey = MetricTelemetry.Cardinalities + private typealias AggregationValue = [String: Double] + /// The sample rate to apply to aggregated metrics. let sampleRate: SampleRate + /// Thread-safe storage for metric aggregations. @ReadWriteLock - private var aggregations: [AggregationKey: Double] = [:] + private var aggregations: [AggregationKey: AggregationValue] = [:] + /// Creates a new metric telemetry aggregator. + /// + /// - Parameter sampleRate: The sample rate to apply to aggregated metrics. + /// Defaults to maximum sample rate (100%). init(sampleRate: SampleRate = .maxSampleRate) { self.sampleRate = sampleRate } + /// Increments a counter metric by a specified value. + /// + /// This method adds the specified value to the current value of the metric. + /// If the metric doesn't exist, it will be initialized with the specified value. + /// + /// - Parameters: + /// - metric: The name of the metric to increment. + /// - value: The amount to increment the metric by. + /// - cardinalities: The dimensions along which the metric will be aggregated. func increment(_ metric: String, by value: Double, cardinalities: MetricTelemetry.Cardinalities) { - _aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] += value } + _aggregations.mutate { aggregations in + var aggregation = aggregations[cardinalities, default: [metric: 0]] + aggregation[metric, default: 0] += value + aggregations[cardinalities] = aggregation + } } + /// Records a gauge metric with a specified value. + /// + /// This method sets the metric to the specified value, replacing any previous value. + /// Gauge metrics are used for values that can fluctuate up and down. + /// + /// - Parameters: + /// - metric: The name of the metric to record. + /// - value: The value to record for the metric. + /// - cardinalities: The dimensions along which the metric will be aggregated. func record(_ metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities) { - _aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] = value } + _aggregations.mutate { aggregations in + var aggregation = aggregations[cardinalities, default: [metric: 0]] + aggregation[metric, default: 0] = value + aggregations[cardinalities] = aggregation + } } + /// Flushes all aggregated metrics and returns them as telemetry events. + /// + /// This method: + /// 1. Converts all aggregated metrics into telemetry events + /// 2. Clears the internal aggregation storage + /// 3. Returns the generated events + /// + /// - Returns: An array of metric telemetry events ready to be sent to the backend. func flush() -> [MetricTelemetry.Event] { - _aggregations.mutate { counters in - defer { counters = [:] } + _aggregations.mutate { aggregations in + defer { aggregations = [:] } - return counters.map { key, value in - var attributes: [String: Encodable] = key.cardinalities - attributes[SDKMetricFields.typeKey] = key.metric - attributes[SDKMetricFields.valueKey] = value + return aggregations.map { key, value in + // Group metrics with same cardinality in the same + // telemetry event + var attributes: [String: Encodable] = key + attributes.merge(value, uniquingKeysWith: { $1 }) return MetricTelemetry.Event( - name: key.metric, + name: value.keys.joined(separator: ","), attributes: attributes, sampleRate: sampleRate ) diff --git a/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift b/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift index 4e34b528a7..e8e6319b01 100644 --- a/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift @@ -37,14 +37,11 @@ class MetricTelemetryAggregatorTests: XCTestCase { // Then let events = aggregator.flush() - XCTAssertEqual(events.count, 4) - - let event1 = try XCTUnwrap(events.first(where: { metric1 == $0.name })) - XCTAssertEqual(event1.attributes["value"] as? Double, Double(iterations)) - - let event2 = try XCTUnwrap(events.first(where: { metric2 == $0.name })) - XCTAssertEqual(event2.attributes["value"] as? Double, Double(iterations)) - + XCTAssertEqual(events.count, 2) + XCTAssertEqual(events.first?.attributes[metric1] as? Double, Double(iterations)) + XCTAssertEqual(events.first?.attributes[metric2] as? Double, Double(iterations)) + XCTAssertEqual(events.last?.attributes[metric1] as? Double, Double(iterations)) + XCTAssertEqual(events.last?.attributes[metric2] as? Double, Double(iterations)) XCTAssertTrue(aggregator.flush().isEmpty) } @@ -76,14 +73,11 @@ class MetricTelemetryAggregatorTests: XCTestCase { // Then let events = aggregator.flush() - XCTAssertEqual(events.count, 4) - - let event1 = try XCTUnwrap(events.first(where: { metric1 == $0.name })) - XCTAssertEqual(event1.attributes["value"] as? Double, value1) - - let event2 = try XCTUnwrap(events.first(where: { metric2 == $0.name })) - XCTAssertEqual(event2.attributes["value"] as? Double, value2) - + XCTAssertEqual(events.count, 2) + XCTAssertEqual(events.first?.attributes[metric1] as? Double, value1) + XCTAssertEqual(events.first?.attributes[metric2] as? Double, value2) + XCTAssertEqual(events.last?.attributes[metric1] as? Double, value1) + XCTAssertEqual(events.last?.attributes[metric2] as? Double, value2) XCTAssertTrue(aggregator.flush().isEmpty) }