diff --git a/DatadogCore/Sources/Core/Storage/Directories.swift b/DatadogCore/Sources/Core/Storage/Directories.swift index 6954f189c4..15507555c2 100644 --- a/DatadogCore/Sources/Core/Storage/Directories.swift +++ b/DatadogCore/Sources/Core/Storage/Directories.swift @@ -9,20 +9,20 @@ import DatadogInternal /// Indicates the main directory for a given instance of the SDK. /// Each instance of `DatadogCore` creates its own `CoreDirectory` to manage data for registered Features. -/// The core directory is created under `/Library/Caches` and uses a name that identifies the certain instance -/// of the SDK (``): +/// The core directory is created under a caller-provided OS root (`osDirectory`) and uses a name that +/// identifies the certain instance of the SDK (``): /// /// ``` -/// /Library/Cache/com.datadoghq/v2// +/// /com.datadoghq/v2// /// ``` /// -/// Note: System may delete data in `/Library/Cache` to free up disk space which reduces the impact on devices working -/// under heavy space pressure. This is intentional for Datadog SDK to have its data purged when system needs more memory -/// for other apps. +/// The root is usually `/Library/Caches` for purgeable Feature data; the system may delete data there to free up +/// disk space, which is intentional for the Datadog SDK. Data that must survive such purges (e.g. remote +/// configuration) is created under `/Library/Application Support` instead. internal struct CoreDirectory { - /// A known OS location the core directory is created within:`/Library/Cache`. + /// The OS location the core directory is created within (e.g. `/Library/Caches` or `/Library/Application Support`). let osDirectory: Directory - /// The core directory specific to this instance of the SDK: `/Library/Cache/com.datadoghq/v2/`. + /// The core directory specific to this instance of the SDK: `/com.datadoghq/v2/`. let coreDirectory: Directory /// Obtains subdirectories for managing batch files for given Feature (creates if don't exist). diff --git a/DatadogCore/Sources/Core/Storage/Files/Directory.swift b/DatadogCore/Sources/Core/Storage/Files/Directory.swift index a0b0d1c9f0..ea6108e299 100644 --- a/DatadogCore/Sources/Core/Storage/Files/Directory.swift +++ b/DatadogCore/Sources/Core/Storage/Files/Directory.swift @@ -188,4 +188,16 @@ extension Directory { } return Directory(url: cachesDirectoryURL) } + + /// Returns `Directory` pointing to `/Library/Application Support`. + /// - Unlike `/Library/Caches`, the system does not purge `/Library/Application Support` under disk pressure, + /// so it is suitable for data that must persist across launches (e.g. remote configuration). + /// - The directory is included in iTunes and iCloud backups by default. + /// - It is not guaranteed to exist; callers create the subdirectories they need. + static func applicationSupport() throws -> Directory { + guard let applicationSupportDirectoryURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + throw InternalError(description: "Cannot obtain `/Library/Application Support/` url.") + } + return Directory(url: applicationSupportDirectoryURL) + } } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 574e0da012..e0fe74c45c 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -424,11 +424,18 @@ extension DatadogCore { ) let httpClient = configuration.httpClientFactory(configuration.proxyConfiguration) - let remoteConfigurationProvider = configuration.remoteConfigurationID.map { - RemoteConfigurationProvider( - id: $0, + let remoteConfigurationProvider = try configuration.remoteConfigurationID.map { id in + // Remote configuration must survive `/Library/Caches` purges, so it is stored under + // `/Library/Application Support` instead of the (purgeable) core directory. + let persistentDirectory = try CoreDirectory( + in: configuration.persistentDirectory(), + instanceName: instanceName, + site: configuration.site + ) + return RemoteConfigurationProvider( + id: id, site: configuration.site, - directory: directory.coreDirectory, + directory: persistentDirectory.coreDirectory, httpClient: httpClient, notificationCenter: configuration.notificationCenter, dateProvider: configuration.dateProvider diff --git a/DatadogCore/Sources/DatadogConfiguration.swift b/DatadogCore/Sources/DatadogConfiguration.swift index 1f967deeda..e13c48845c 100644 --- a/DatadogCore/Sources/DatadogConfiguration.swift +++ b/DatadogCore/Sources/DatadogConfiguration.swift @@ -224,6 +224,10 @@ extension Datadog { /// All instances of the SDK use the same root folder, but each creates its own subdirectory. internal var systemDirectory: () throws -> Directory = { try Directory.cache() } + /// Obtains OS directory where the SDK stores data that must survive `/Library/Caches` purges + /// (e.g. remote configuration). Backed by `/Library/Application Support`. + internal var persistentDirectory: () throws -> Directory = { try Directory.applicationSupport() } + /// Default process information. internal var processInfo: ProcessInfo = .processInfo diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift index b73cf8ee70..1427f0bf9f 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift @@ -20,6 +20,14 @@ class DirectoryTests: XCTestCase { XCTAssertTrue(fileManager.fileExists(atPath: directory.url.path)) } + func testItObtainsApplicationSupportDirectory() throws { + let directory = try Directory.applicationSupport() + let expectedURL = try XCTUnwrap(fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first) + // Unlike `/Library/Caches`, `/Library/Application Support` is not guaranteed to exist, + // so we assert on the resolved URL rather than its existence on disk. + XCTAssertEqual(directory.url, expectedURL) + } + func testGivenSubdirectoryName_itCreatesIt() throws { let directory = try Directory.cache().createSubdirectory(path: uniqueSubdirectoryName()) defer { directory.delete() } diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index 85dba93f22..79a9c673a5 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -549,6 +549,33 @@ class DatadogTests: XCTestCase { XCTAssertNotNil(core.remoteConfigurationProvider) } + func testGivenRemoteConfigurationID_itIsStoredInPersistentDirectoryNotCaches() throws { + // Given — distinct injected locations for purgeable caches vs. persistent storage + let cachesDirectory = Directory(url: obtainUniqueTemporaryDirectory()) + let persistentDirectory = Directory(url: obtainUniqueTemporaryDirectory()) + var config = defaultConfig + config.remoteConfigurationID = "test-id" + config.systemDirectory = { cachesDirectory } + config.persistentDirectory = { persistentDirectory } + config.httpClientFactory = { _ in HTTPClientMock() } + + // When + Datadog.initialize(with: config, trackingConsent: .granted) + defer { Datadog.flushAndDeinitialize() } + + // Then — remote configuration must live under Application Support, never the purgeable caches + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let rcPath = try XCTUnwrap(core.remoteConfigurationProvider?.directory.url.path) + XCTAssertTrue( + rcPath.hasPrefix(persistentDirectory.url.path), + "Remote configuration must be stored under the persistent (Application Support) directory" + ) + XCTAssertFalse( + rcPath.hasPrefix(cachesDirectory.url.path), + "Remote configuration must not be stored under the purgeable caches directory" + ) + } + func testCustomSDKInstance() throws { // When Datadog.initialize(