-
Notifications
You must be signed in to change notification settings - Fork 167
[RUM-16084] Fetch and store remote configuration file at SDK init #2919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
saraSr5
wants to merge
10
commits into
feature/remote-config
Choose a base branch
from
saraSr5/RUM-16084/remote-config
base: feature/remote-config
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8eb296a
[RUM-16084] Add remote configuration fetch and cache
saraSr5 a0e3c93
[RUM-16084] Improve telemetry and add comments
saraSr5 c5d075a
[RUM-16084] Fix lint, update CHANGELOG
saraSr5 0661d12
[RUM-16084] ObjC bridge, SPI visibility, cache consistency
saraSr5 ae5cd4b
[RUM-16084] Fix URL encoding, cache consistency, and test determinism
saraSr5 fe567dc
[RUM-16084] Fix CHANGELOG PR reference to #2919
saraSr5 dcab0d4
[RUM-16084] Build proxy-aware URLSession for remote config fetch
saraSr5 d62ca81
[RUM-16084] Empty ID guard, comment fix, save failure telemetry
saraSr5 e51bd7d
[RUM-16084] Trim whitespace/newlines from remoteConfigurationID, fix …
saraSr5 6485224
[RUM-16084] Remove force-unwrap from remoteConfigurationURL
saraSr5 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| 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
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apply the configured
DataEncryptionto 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 leavesremote-config.jsonin plaintext even when apps opt into encryption for persisted SDK data.Useful? React with 👍 / 👎.