[RUM-16084] Fetch and store remote configuration file at SDK init#2919
[RUM-16084] Fetch and store remote configuration file at SDK init#2919saraSr5 wants to merge 10 commits into
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe567dc579
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /// The URLSession used for remote configuration fetching. Replaceable in tests. | ||
| /// TODO: Build this session from `proxyConfiguration` so remote config fetches respect | ||
| /// proxy settings in restricted network environments (same as the main HTTP client). | ||
| internal var remoteConfigurationSession = URLSession(configuration: .ephemeral) |
There was a problem hiding this comment.
Honor proxy settings for remote config fetch session
Create the remote-config URLSession from proxyConfiguration before startup fetches run; using a hard-coded ephemeral session here bypasses the SDK’s proxy setup, so apps in environments that require an explicit proxy can upload telemetry successfully but will consistently fail to retrieve remote config. This is a functional regression for customers relying on Datadog.Configuration.proxyConfiguration.
Useful? React with 👍 / 👎.
| /// the two in sync. | ||
| func save(_ data: Data) { | ||
| do { | ||
| try data.write(to: fileURL, options: .atomic) |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
DatadogInternal/Sources/Context/DatadogSite.swift:66
- The force unwrap of
endpoint.host!can still crash ifendpointever changes (or is constructed differently) and lacks a host. Since this method already returnsURL?, it would be safer toguard let host = endpoint.hostand returnnil(and let the caller report telemetry) rather than force-unwrapping.
let pathSegmentAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: "/?#"))
guard let encoded = id.addingPercentEncoding(withAllowedCharacters: pathSegmentAllowed) else {
return nil
}
// swiftlint:disable:next force_unwrapping
return URL(string: "https://sdk-configuration.\(endpoint.host!)/v1/\(encoded).json")
| /// - Returns: URL to GET the config JSON from, or `nil` if `id` cannot be percent-encoded. | ||
| @_spi(Internal) | ||
| public func remoteConfigurationURL(for id: String) -> URL? { | ||
| // Format: https://sdk-configuration.browser-intake-{site}/v1/{id}.json | ||
| // `.urlPathAllowed` leaves `/`, `?`, and `#` unencoded (they are legal in a URL). | ||
| // Subtract them so an ID containing those characters doesn't produce extra path | ||
| // segments, a query string, or a fragment. | ||
| let pathSegmentAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: "/?#")) | ||
| guard let encoded = id.addingPercentEncoding(withAllowedCharacters: pathSegmentAllowed) else { | ||
| return nil | ||
| } | ||
| // swiftlint:disable:next force_unwrapping | ||
| return URL(string: "https://sdk-configuration.\(endpoint.host!)/v1/\(encoded).json") |
| // Trigger remote config fetch if an ID was provided. | ||
| // The fetch is async — init returns immediately and does not wait for it. | ||
| if let id = configuration.remoteConfigurationID { | ||
| 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)'") | ||
| } | ||
| } |
| /// Created at init using this core's directory. `data` is `nil` until a | ||
| /// successful CDN fetch has been written and the app relaunched. |
| func save(_ data: Data) { | ||
| do { | ||
| try data.write(to: fileURL, options: .atomic) | ||
| self.data = data | ||
| } catch { | ||
| // Best-effort: disk write failures are silently ignored. | ||
| // self.data is intentionally NOT updated so in-memory state stays | ||
| // consistent with what is actually on disk. | ||
| } |
| // `.urlPathAllowed` leaves `/`, `?`, and `#` unencoded (they are legal in a URL). | ||
| // Subtract them so an ID containing those characters doesn't produce extra path | ||
| // segments, a query string, or a fragment. | ||
| let pathSegmentAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: "/?#")) |
|
|
||
| /// Constructs the CDN URL for fetching the remote configuration document. | ||
| /// - Parameter id: The value of `Datadog.Configuration.remoteConfigurationID`. | ||
| /// - Returns: URL to GET the config JSON from, or `nil` if `id` cannot be percent-encoded. |
| /// - Returns: `true` if the write succeeded, `false` otherwise. | ||
| @discardableResult | ||
| func save(_ data: Data) -> Bool { | ||
| do { | ||
| try data.write(to: fileURL, options: .atomic) | ||
| 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 | ||
| } |
| public protocol FlagsClientProtocol: AnyObject | ||
| var state: FlagsStateObservable | ||
| func setEvaluationContext(_ context: FlagsEvaluationContext,completion: @escaping (Result<Void, FlagsError>) -> Void) | ||
| func getDetails<T>(key: String, defaultValue: T) -> FlagDetails<T> where T: Equatable, T: FlagValue | ||
| [?] extension FlagsClientProtocol | ||
| public var state: FlagsStateObservable | ||
| [?] extension FlagsClientProtocol | ||
| public func setEvaluationContext(_ context: FlagsEvaluationContext) | ||
| public func setEvaluationContext(_ context: FlagsEvaluationContext) async throws | ||
| [?] extension FlagsClientProtocol | ||
| public func getValue<T>(key: String, defaultValue: T) -> T where T: Equatable, T: FlagValue | ||
| public func getBooleanValue(key: String, defaultValue: Bool) -> Bool | ||
| public func getStringValue(key: String, defaultValue: String) -> String | ||
| public func getIntegerValue(key: String, defaultValue: Int) -> Int | ||
| public func getDoubleValue(key: String, defaultValue: Double) -> Double | ||
| public func getObjectValue(key: String, defaultValue: AnyValue) -> AnyValue | ||
| [?] extension FlagsClientProtocol | ||
| public func getBooleanDetails(key: String, defaultValue: Bool) -> FlagDetails<Bool> | ||
| public func getStringDetails(key: String, defaultValue: String) -> FlagDetails<String> | ||
| public func getIntegerDetails(key: String, defaultValue: Int) -> FlagDetails<Int> | ||
| public func getDoubleDetails(key: String, defaultValue: Double) -> FlagDetails<Double> | ||
| public func getObjectDetails(key: String, defaultValue: AnyValue) -> FlagDetails<AnyValue> | ||
| public enum FlagsClientState: Equatable | ||
| public enum FlagsClientState: Sendable |
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
| // swiftlint:disable:next force_unwrapping | ||
| return URL(string: "https://sdk-configuration.\(endpoint.host!)/v1/\(encoded).json") |
| // `.urlPathAllowed` leaves `/`, `?`, and `#` unencoded (they are legal in a URL). | ||
| // Subtract them so an ID containing those characters doesn't produce extra path | ||
| // segments, a query string, or a fragment. | ||
| let pathSegmentAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: "/?#")) |
| public func getDoubleDetails(key: String, defaultValue: Double) -> FlagDetails<Double> | ||
| public func getObjectDetails(key: String, defaultValue: AnyValue) -> FlagDetails<AnyValue> | ||
| public enum FlagsClientState: Equatable | ||
| public enum FlagsClientState: Sendable | ||
| case notReady |
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
What and why?
Implements the first building block of the Remote configuration: fetching the remote config document from the Datadog CDN at SDK startup and caching the raw JSON to disk.
When a
remoteConfigurationIDis provided inDatadog.Configuration, the SDK asynchronously fetches the corresponding config document and writes it atomically toremote-config.jsoninside the SDK's private core directory. On any failure (network error, non-2xx, empty body, invalid JSON), a telemetry error is reported and any existing cached file is left untouched.Parsing the cached file and applying its values to SDK configuration is out of scope for this PR.
How?
remoteConfigurationID: String?toDatadog.Configuration(defaults tonil, no behaviour change for existing apps)DatadogSite.remoteConfigurationURL(for:)to build the CDN URL from the ID and resolved siteRemoteConfigurationCache— thin wrapper around an atomic file write/read in the core directoryRemoteConfigurationFetcher— fires a one-shotURLSessiondataTask, validates the response (2xx, non-empty, valid JSON), and delegates storage to the cacheDatadog.initialize()— fire-and-forget, does not block SDK initReview checklist
make api-surfacewhen adding new APIs