From 62c7574999a5a3c6750feaf7d5473a3409a88428 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Tue, 10 Dec 2024 12:27:39 -0300 Subject: [PATCH 1/2] Caching remote config to disk --- .../EmbraceConfigurable/RemoteConfig.swift | 44 ++++++++++++++++++- .../RemoteConfig/RemoteConfig+Options.swift | 4 ++ .../RemoteConfig/RemoteConfigFetcher.swift | 14 +++--- .../FileSystem/EmbraceFileSystem.swift | 16 ++++++- .../EmbraceCore/Internal/Embrace+Config.swift | 3 +- .../RemoteConfigFetcherTests.swift | 1 + .../RemoteConfigTests.swift | 25 +---------- 7 files changed, 72 insertions(+), 35 deletions(-) diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift index ba98244a..21932c8f 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift @@ -9,6 +9,8 @@ import EmbraceConfiguration /// Remote config uses the Embrace Config Service to request config values public class RemoteConfig { + let logger: InternalLogger + // config requests @ThreadSafe var payload: RemoteConfigPayload let fetcher: RemoteConfigFetcher @@ -19,6 +21,8 @@ public class RemoteConfig { @ThreadSafe private(set) var updating = false + let cacheURL: URL? + public convenience init( options: RemoteConfig.Options, payload: RemoteConfigPayload = RemoteConfigPayload(), @@ -38,6 +42,42 @@ public class RemoteConfig { self.payload = payload self.fetcher = fetcher self.deviceIdHexValue = options.deviceId.intValue(digitCount: Self.deviceIdUsedDigits) + self.logger = logger + + if let url = options.cacheLocation { + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + self.cacheURL = options.cacheLocation?.appendingPathComponent("cache") + loadFromCache() + } else { + self.cacheURL = nil + } + } + + func loadFromCache() { + guard let url = cacheURL, + FileManager.default.fileExists(atPath: url.path) else { + return + } + + do { + let data = try Data(contentsOf: url) + payload = try JSONDecoder().decode(RemoteConfigPayload.self, from: data) + } catch { + logger.error("Error loading cached remote config!") + } + } + + func saveToCache(_ data: Data?) { + guard let url = cacheURL, + let data = data else { + return + } + + do { + try data.write(to: url, options: .atomic) + } catch { + logger.warning("Error saving remote config cache!") + } } } @@ -67,7 +107,7 @@ extension RemoteConfig: EmbraceConfigurable { } updating = true - fetcher.fetch { [weak self] newPayload in + fetcher.fetch { [weak self] newPayload, data in defer { self?.updating = false } guard let strongSelf = self else { completion(false, nil) @@ -82,6 +122,8 @@ extension RemoteConfig: EmbraceConfigurable { let didUpdate = strongSelf.payload != newPayload strongSelf.payload = newPayload + strongSelf.saveToCache(data) + completion(didUpdate, nil) } } diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift index 6961bcab..14bf43c8 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift @@ -17,6 +17,8 @@ public extension RemoteConfig { let appVersion: String let userAgent: String + let cacheLocation: URL? + let urlSessionConfiguration: URLSessionConfiguration public init( @@ -28,6 +30,7 @@ public extension RemoteConfig { sdkVersion: String, appVersion: String, userAgent: String, + cacheLocation: URL?, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default ) { self.apiBaseUrl = apiBaseUrl @@ -38,6 +41,7 @@ public extension RemoteConfig { self.sdkVersion = sdkVersion self.appVersion = appVersion self.userAgent = userAgent + self.cacheLocation = cacheLocation self.urlSessionConfiguration = urlSessionConfiguration } } diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift index abfc27d6..d7b532ac 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift @@ -27,9 +27,9 @@ class RemoteConfigFetcher { ) } - func fetch(completion: @escaping (RemoteConfigPayload?) -> Void) { + func fetch(completion: @escaping (RemoteConfigPayload?, Data?) -> Void) { guard let request = newRequest() else { - completion(nil) + completion(nil, nil) return } @@ -38,19 +38,19 @@ class RemoteConfigFetcher { guard let data = data, error == nil else { self?.logger.error("Error fetching remote config:\n\(String(describing: error?.localizedDescription))") - completion(nil) + completion(nil, nil) return } guard let httpResponse = response as? HTTPURLResponse else { self?.logger.error("Error fetching remote config - Invalid response:\n\(String(describing: response?.description))") - completion(nil) + completion(nil, nil) return } guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { self?.logger.error("Error fetching remote config - Invalid response:\n\(httpResponse.description))") - completion(nil) + completion(nil, nil) return } @@ -59,11 +59,11 @@ class RemoteConfigFetcher { let payload = try JSONDecoder().decode(RemoteConfigPayload.self, from: data) self?.logger.info("Successfully fetched remote config") - completion(payload) + completion(payload, data) } catch { self?.logger.error("Error decoding remote config:\n\(error.localizedDescription)") // if a decoding issue happens, instead of returning `nil`, we provide a default `RemoteConfigPayload` - completion(RemoteConfigPayload()) + completion(RemoteConfigPayload(), nil) } } diff --git a/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift b/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift index 593eec75..4c89612b 100644 --- a/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift +++ b/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift @@ -11,6 +11,7 @@ public struct EmbraceFileSystem { static let uploadsDirectoryName = "uploads" static let crashesDirectoryName = "crashes" static let captureDirectoryName = "capture" + static let configDirectoryName = "config" static let defaultPartitionId = "default" @@ -53,8 +54,7 @@ public struct EmbraceFileSystem { /// ``` /// - Parameters: /// - name: The name of the subdirectory - /// - partitionIdentifier: The main partition identifier to use - /// identifier to use + /// - partitionId: The main partition identifier to use /// - appGroupId: The app group identifier if using an app group container. static func directoryURL(name: String, partitionId: String, appGroupId: String? = nil) -> URL? { guard let baseURL = systemDirectory(appGroupId: appGroupId) else { @@ -104,4 +104,16 @@ public struct EmbraceFileSystem { appGroupId: appGroupId ) } + + /// Returns the subdirectory for config cache + /// ``` + /// io.embrace.data///config + /// ``` + static func configDirectoryURL(partitionIdentifier: String, appGroupId: String? = nil) -> URL? { + return directoryURL( + name: configDirectoryName, + partitionId: partitionIdentifier, + appGroupId: appGroupId + ) + } } diff --git a/Sources/EmbraceCore/Internal/Embrace+Config.swift b/Sources/EmbraceCore/Internal/Embrace+Config.swift index 41501dfa..e71f691c 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Config.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Config.swift @@ -47,7 +47,8 @@ extension Embrace { osVersion: EMBDevice.appVersion ?? "", sdkVersion: EmbraceMeta.sdkVersion, appVersion: EMBDevice.operatingSystemVersion, - userAgent: EmbraceMeta.userAgent + userAgent: EmbraceMeta.userAgent, + cacheLocation: EmbraceFileSystem.configDirectoryURL(partitionIdentifier: appId, appGroupId: options.appGroupId) ) return RemoteConfig( diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift index a16a271c..670d7fe2 100644 --- a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift @@ -40,6 +40,7 @@ class RemoteConfigFetcherTests: XCTestCase { sdkVersion: sdkVersion, appVersion: appVersion, userAgent: userAgent, + cacheLocation: nil, urlSessionConfiguration: Self.urlSessionConfig ) } diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift index f0e16297..557fb00d 100644 --- a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift @@ -21,33 +21,10 @@ final class RemoteConfigTests: XCTestCase { sdkVersion: TestConstants.sdkVersion, appVersion: TestConstants.appVersion, userAgent: TestConstants.userAgent, + cacheLocation: nil, urlSessionConfiguration: URLSessionConfiguration.default ) - func mockSuccessfulResponse() throws { - var url = try XCTUnwrap(URL(string: "\(options.apiBaseUrl)/v2/config")) - - if #available(iOS 16.0, watchOS 9.0, *) { - url.append(queryItems: [ - .init(name: "appId", value: options.appId), - .init(name: "osVersion", value: options.osVersion), - .init(name: "appVersion", value: options.appVersion), - .init(name: "deviceId", value: options.deviceId.hex), - .init(name: "sdkVersion", value: options.sdkVersion) - ]) - } else { - XCTFail("This will fail on versions prior to iOS 16.0") - } - - let path = Bundle.module.path( - forResource: "remote_config", - ofType: "json", - inDirectory: "Fixtures" - )! - let data = try Data(contentsOf: URL(fileURLWithPath: path)) - EmbraceHTTPMock.mock(url: url, response: .withData(data, statusCode: 200)) - } - // MARK: Tests func test_isEnabled_returnsCorrectValues() { From a1f013a4998fc06b3538a29d349fa2643bbd0f37 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Tue, 10 Dec 2024 12:47:30 -0300 Subject: [PATCH 2/2] Fixing tests --- .../RemoteConfig/RemoteConfigFetcherTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift index 670d7fe2..07f86164 100644 --- a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift @@ -146,7 +146,7 @@ class RemoteConfigFetcherTests: XCTestCase { let fetcher = RemoteConfigFetcher(options: options, logger: logger) let expectation = expectation(description: "URL request") - fetcher.fetch { payload in + fetcher.fetch { payload, data in XCTAssertNotNil(payload) expectation.fulfill() } @@ -164,7 +164,7 @@ class RemoteConfigFetcherTests: XCTestCase { let fetcher = RemoteConfigFetcher(options: options, logger: logger) let expectation = expectation(description: "URL request") - fetcher.fetch { payload in + fetcher.fetch { payload, data in XCTAssertNil(payload) expectation.fulfill() }