Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

- [IMPROVEMENT] Add support for `maui` source for cross-platform RUM events from .NET MAUI applications. See [#2891][]
- [FEATURE] Add `remoteConfigurationID` to `Datadog.Configuration` to fetch and cache the remote configuration document from the Datadog CDN at SDK startup. See [#2919][]
- [FEATURE] Add client state management to `DatadogFlags` module. See [#2719][]
- [IMPROVEMENT] Skip malformed RUM attributes individually instead of dropping the entire event, and log clear error messages. See [#2844][]
- [FIX] Propagate native `anonymous_id` to WebView RUM and Log events. See [#2847][]
Expand Down Expand Up @@ -1143,6 +1144,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#2856]: https://github.com/DataDog/dd-sdk-ios/pull/2856
[#2866]: https://github.com/DataDog/dd-sdk-ios/pull/2866
[#2891]: https://github.com/DataDog/dd-sdk-ios/pull/2891
[#2919]: https://github.com/DataDog/dd-sdk-ios/pull/2919

[@00fa9a]: https://github.com/00FA9A
[@britton-earnin]: https://github.com/Britton-Earnin
Expand Down
20 changes: 20 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
09A936A02F0EB989000B6379 /* SamplingMechanismType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A9369A2F0EB989000B6379 /* SamplingMechanismType.swift */; };
09A936A12F0EB989000B6379 /* SamplingPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A9369B2F0EB989000B6379 /* SamplingPriority.swift */; };
09A936A22F0EB989000B6379 /* SpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A9369C2F0EB989000B6379 /* SpanContext.swift */; };
1019DFB52FA38A54006599B4 /* RemoteConfigurationCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1019DFB42FA38A4C006599B4 /* RemoteConfigurationCacheTests.swift */; };
1019DFB72FA38AC9006599B4 /* RemoteConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1019DFB62FA38AC3006599B4 /* RemoteConfigurationCache.swift */; };
1019DFB92FA390A9006599B4 /* RemoteConfigurationFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1019DFB82FA39077006599B4 /* RemoteConfigurationFetcherTests.swift */; };
1019DFBB2FA3910A006599B4 /* RemoteConfigurationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1019DFBA2FA39108006599B4 /* RemoteConfigurationFetcher.swift */; };
1019DFD92FB1BB2E006599B4 /* DatadogSiteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1019DFD82FB1BB23006599B4 /* DatadogSiteTests.swift */; };
11030D5F2D959EAD00732D5F /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; };
11030D6D2D95A48B00732D5F /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; };
11030D772D96EC5C00732D5F /* ViewHitchesMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11030D752D96EC5300732D5F /* ViewHitchesMetric.swift */; };
Expand Down Expand Up @@ -1770,6 +1775,11 @@
09A9369B2F0EB989000B6379 /* SamplingPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplingPriority.swift; sourceTree = "<group>"; };
09A9369C2F0EB989000B6379 /* SpanContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanContext.swift; sourceTree = "<group>"; };
0D2A20EFB1D82D0B8AC781F9 /* HeatmapMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeatmapMocks.swift; sourceTree = "<group>"; };
1019DFB42FA38A4C006599B4 /* RemoteConfigurationCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigurationCacheTests.swift; sourceTree = "<group>"; };
1019DFB62FA38AC3006599B4 /* RemoteConfigurationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigurationCache.swift; sourceTree = "<group>"; };
1019DFB82FA39077006599B4 /* RemoteConfigurationFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigurationFetcherTests.swift; sourceTree = "<group>"; };
1019DFBA2FA39108006599B4 /* RemoteConfigurationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigurationFetcher.swift; sourceTree = "<group>"; };
1019DFD82FB1BB23006599B4 /* DatadogSiteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogSiteTests.swift; sourceTree = "<group>"; };
11014EACF1FB9927DAD57822 /* EvaluationMocks.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvaluationMocks.swift; sourceTree = "<group>"; };
11030D752D96EC5300732D5F /* ViewHitchesMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHitchesMetric.swift; sourceTree = "<group>"; };
110311752EF96ED000750DD4 /* DDLogs+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDLogs+apiTests.m"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4456,6 +4466,8 @@
61133B9E2423979B00786299 /* Core */ = {
isa = PBXGroup;
children = (
1019DFBA2FA39108006599B4 /* RemoteConfigurationFetcher.swift */,
1019DFB62FA38AC3006599B4 /* RemoteConfigurationCache.swift */,
D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */,
D214DAA429E072D7004D0AE8 /* MessageBus.swift */,
D2EFA866286DA82700F1FAA6 /* Context */,
Expand Down Expand Up @@ -4582,6 +4594,8 @@
61133C212423990D00786299 /* Core */ = {
isa = PBXGroup;
children = (
1019DFB82FA39077006599B4 /* RemoteConfigurationFetcherTests.swift */,
1019DFB42FA38A4C006599B4 /* RemoteConfigurationCacheTests.swift */,
61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */,
61EF78C0257F842000EDCCB3 /* FeatureTests.swift */,
61345612244756E300E7DA6B /* PerformancePresetTests.swift */,
Expand Down Expand Up @@ -6621,6 +6635,7 @@
D2DA239C298D58F300C6C7E6 /* Context */ = {
isa = PBXGroup;
children = (
1019DFD82FB1BB23006599B4 /* DatadogSiteTests.swift */,
D2DA239D298D58F300C6C7E6 /* AppStateHistoryTests.swift */,
D2DA239E298D58F300C6C7E6 /* DeviceInfoTests.swift */,
6174D6152BFDF29B00EC7469 /* BundleTypeTests.swift */,
Expand Down Expand Up @@ -7881,12 +7896,14 @@
D2A7840F29A53B2F003B03BB /* Directory.swift in Sources */,
61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */,
D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */,
1019DFB72FA38AC9006599B4 /* RemoteConfigurationCache.swift in Sources */,
614396722A67D74F00197326 /* BatchMetrics.swift in Sources */,
D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */,
613E792F2577B0F900DFCC17 /* Reader.swift in Sources */,
A731B8042EE1DD0D003D1E4F /* CrossPlatformExtension.swift in Sources */,
61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */,
D2DC4BF627F484AA00E4FB96 /* DataEncryption.swift in Sources */,
1019DFBB2FA3910A006599B4 /* RemoteConfigurationFetcher.swift in Sources */,
11EA5C2E2DC3CCA500E8DFA2 /* DatadogConfiguration.swift in Sources */,
D2FB1254292E0E96005B13F8 /* TrackingConsentPublisher.swift in Sources */,
61D3E0D6277B23F1008BE766 /* KronosClock.swift in Sources */,
Expand Down Expand Up @@ -7950,6 +7967,7 @@
61133C5A2423990D00786299 /* FileTests.swift in Sources */,
61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */,
D2EFA875286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */,
1019DFB92FA390A9006599B4 /* RemoteConfigurationFetcherTests.swift in Sources */,
61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */,
A727C4BB2BADB3AB00707DFD /* DDSessionReplay+apiTests.m in Sources */,
110311762EF96F6700750DD4 /* DDLogs+apiTests.m in Sources */,
Expand Down Expand Up @@ -8014,6 +8032,7 @@
1132B34D2D9E9A30002384F2 /* RUMViewHitchesMetricIntegrationTests.swift in Sources */,
D2A1EE442886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */,
D29A9FD029DDC58E005C54A4 /* RUMFeatureTests.swift in Sources */,
1019DFB52FA38A54006599B4 /* RemoteConfigurationCacheTests.swift in Sources */,
61F930C82BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */,
D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */,
61133C4B2423990D00786299 /* DDLogsTests.swift in Sources */,
Expand Down Expand Up @@ -8988,6 +9007,7 @@
116F84062CFDD06700705755 /* SampleRateTests.swift in Sources */,
3C0D5DF52A5443B100446CF9 /* DataFormatTests.swift in Sources */,
D2EBEE4429BA168200B15732 /* TraceIDTests.swift in Sources */,
1019DFD92FB1BB2E006599B4 /* DatadogSiteTests.swift in Sources */,
D2EBEE4329BA168200B15732 /* TraceIDGeneratorTests.swift in Sources */,
D2DA23A7298D58F400C6C7E6 /* AppStateHistoryTests.swift in Sources */,
D2EBEE3D29BA163E00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */,
Expand Down
22 changes: 21 additions & 1 deletion DatadogCore/Sources/Core/DatadogCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ internal final class DatadogCore {
/// The message-bus instance.
let bus = MessageBus()

/// Cache of the remote configuration JSON fetched from the CDN.
/// Created at init using this core's directory. `data` is `nil` on first
/// launch (or when `remoteConfigurationID` was never set); populated either
/// from disk (previous successful fetch) or in-memory after the first
/// successful fetch in the current session.
private let remoteConfigCache: RemoteConfigurationCache

/// Registry for Features.
@ReadWriteLock
private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:]
Expand Down Expand Up @@ -114,7 +121,7 @@ internal final class DatadogCore {
self.contextProvider.subscribe(\.accountInfo, to: accountInfoPublisher)
self.contextProvider.subscribe(\.version, to: applicationVersionPublisher)
self.contextProvider.subscribe(\.trackingConsent, to: consentPublisher)

self.remoteConfigCache = RemoteConfigurationCache(directory: directory.coreDirectory)
// connect the core to the message bus.
// the bus will keep a weak ref to the core.
bus.connect(core: self)
Expand Down Expand Up @@ -255,6 +262,19 @@ internal final class DatadogCore {
allDataStores.forEach { $0.clearAllData() }
}

/// Fetches the remote configuration document from the CDN and caches it to disk.
///
/// Called from `Datadog.initialize()` when `remoteConfigurationID` is set.
/// The fetch is fire-and-forget — SDK init does not wait for it.
internal func fetchRemoteConfiguration(from endpoint: URL, session: URLSession) {
let fetcher = RemoteConfigurationFetcher(
cache: remoteConfigCache,
telemetry: telemetry,
session: session
)
fetcher.fetch(from: endpoint)
}

/// Adds a message receiver to the bus.
///
/// After being added to the bus, the core will send the current context to receiver.
Expand Down
66 changes: 66 additions & 0 deletions DatadogCore/Sources/Core/RemoteConfigurationCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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

/// Manages the on-disk cache of the remote configuration JSON document.
///
/// The cache is a single file `remote-config.json` stored at the root of
/// the SDK's private core directory:
///
/// /Library/Caches/com.datadoghq/v2/<instance-uuid>/remote-config.json
///
/// The file contains raw JSON bytes exactly as received from the CDN.
/// Parsing and applying those values is handled separately.
internal final class RemoteConfigurationCache {
private static let fileName = "remote-config.json"

private let fileURL: URL

/// Raw JSON bytes from the previous CDN fetch, read synchronously at init.
/// `nil` when no cache exists yet (first launch, or remoteConfigurationID was never set).
/// Consumed by the config-application layer once parsing and applying remote values is implemented.
private(set) var data: Data?

init(directory: Directory) {
self.fileURL = directory.url.appendingPathComponent(Self.fileName)
// Synchronous read on the caller's thread (main thread during SDK init).
// Acceptable because the file is small (a single JSON document) and only
// present after a previous successful fetch — absent on first launch.
self.data = Self.readFromDisk(at: fileURL)
}

// MARK: - Private

private static func readFromDisk(at url: URL) -> Data? {
guard FileManager.default.fileExists(atPath: url.path)
else {
return nil
}
return try? Data(contentsOf: url)
}

// MARK: - Internal

/// Writes raw CDN response bytes to disk atomically and updates the in-memory copy.
/// Called only on a successful CDN response — never on failure.
/// In-memory `data` is only updated when the disk write succeeds, keeping
/// the two in sync.
/// - Returns: `true` if the write succeeded, `false` otherwise.
@discardableResult
func save(_ data: Data) -> Bool {
do {
try data.write(to: fileURL, options: .atomic)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Encrypt cached remote config before writing to disk

Apply the configured DataEncryption to this payload before persisting it: the new cache currently writes raw JSON bytes directly to disk, which bypasses the SDK’s on-disk encryption contract and leaves remote-config.json in plaintext even when apps opt into encryption for persisted SDK data.

Useful? React with 👍 / 👎.

self.data = data
return true
} catch {
// self.data is intentionally NOT updated so in-memory state stays
// consistent with what is actually on disk.
return false
}
Comment on lines +53 to +64
}
}
86 changes: 86 additions & 0 deletions DatadogCore/Sources/Core/RemoteConfigurationFetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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

/// Fetches the remote configuration JSON document from the CDN and delegates
/// storage to `RemoteConfigurationCache`.
///
/// Rules:
/// - Fetch is always asynchronous — never blocks the caller.
/// - On success (2xx, non-empty body, valid JSON): calls `cache.save(_:)`.
/// - On any failure: reports a telemetry error and leaves the existing cache untouched.
internal final class RemoteConfigurationFetcher {
private let cache: RemoteConfigurationCache
private let telemetry: Telemetry
private let session: URLSession

init(
cache: RemoteConfigurationCache,
telemetry: Telemetry,
session: URLSession = URLSession(configuration: .ephemeral)
) {
self.cache = cache
self.telemetry = telemetry
self.session = session
}

/// Fires a background GET request to `endpoint`.
///
/// - Parameter endpoint: The CDN URL to fetch from.
/// - Parameter didComplete: Called when the fetch (and any write) is done.
/// Pass `nil` in production; inject a closure in tests to await completion.
func fetch(from endpoint: URL, didComplete: (() -> Void)? = nil) {
let cache = self.cache
let telemetry = self.telemetry
let task = session.dataTask(with: endpoint) { data, response, error in
defer { didComplete?() }

// 1. Network error
if let error = error {
telemetry.error("[RemoteConfig] Network error", error: error)
return
}

// 2. Non-2xx HTTP status
guard let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) else {
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
// Use a fixed message so all HTTP errors bucket together in telemetry;
// the status code lives in the error object, not the message string.
telemetry.error("[RemoteConfig] Non-2xx response", error: NSError(
domain: "RemoteConfiguration",
code: code,
userInfo: [NSLocalizedDescriptionKey: "HTTP \(code)"]
))
return
}

// 3. Empty body
guard let data = data, !data.isEmpty else {
telemetry.error("[RemoteConfig] Empty response body")
return
}

// 4. Invalid JSON
// Intentional allocate-and-discard: we only need to validate the bytes
// are well-formed JSON before caching. The parsed object is thrown away.
// This guarantees the cache never contains non-JSON data, so future
// parsing layers can trust the cached bytes without re-validating.
guard (try? JSONSerialization.jsonObject(with: data)) != nil else {
telemetry.error("[RemoteConfig] Response is not valid JSON")
return
}

// All checks passed — persist to disk
if !cache.save(data) {
telemetry.error("[RemoteConfig] Failed to write remote configuration to disk")
}
}
task.resume()
}
}
21 changes: 21 additions & 0 deletions DatadogCore/Sources/Datadog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import Foundation
@_spi(Internal)
import DatadogInternal

//swiftlint:disable duplicate_imports
Expand Down Expand Up @@ -297,6 +298,26 @@ public enum Datadog {
CITestIntegration.active?.startIntegration()

CoreRegistry.register(core, named: instanceName)

// Trigger remote config fetch if an ID was provided.
// The fetch is async — init returns immediately and does not wait for it.
if let rawID = configuration.remoteConfigurationID {
let id = rawID.trimmingCharacters(in: .whitespacesAndNewlines)
if id.isEmpty {
core.telemetry.error("[RemoteConfig] remoteConfigurationID must not be blank")
} else if let endpoint = configuration.site.remoteConfigurationURL(for: id) {
let session = configuration.remoteConfigurationSession ?? {
let sessionConfig: URLSessionConfiguration = .ephemeral
sessionConfig.urlCache = nil
sessionConfig.connectionProxyDictionary = configuration.proxyConfiguration
return URLSession(configuration: sessionConfig)
}()
core.fetchRemoteConfiguration(from: endpoint, session: session)
} else {
core.telemetry.error("[RemoteConfig] Could not build CDN URL for remoteConfigurationID '\(id)'")
}
}
Comment on lines +302 to +319

deleteV1Folders(in: core)

DD.logger = InternalLogger(
Expand Down
9 changes: 9 additions & 0 deletions DatadogCore/Sources/DatadogConfiguration+objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ public final class objc_Configuration: NSObject {
set { sdkConfiguration.backgroundTasksEnabled = newValue }
}

/// The remote configuration ID used to fetch SDK settings from the Datadog CDN at startup.
///
/// When non-nil, the SDK asynchronously fetches and caches the remote configuration document.
/// Default is `nil` — no fetch is performed.
public var remoteConfigurationID: String? {
get { sdkConfiguration.remoteConfigurationID }
set { sdkConfiguration.remoteConfigurationID = newValue }
}

/// Creates a Datadog SDK Configuration object.
///
/// - Parameters:
Expand Down
Loading