diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cdde085c8..d24b8927d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - [FEATURE] Enable DatadogCore, DatadogLogs and DatadogTrace to compile on watchOS platform. See [#1918][] (Thanks [@jfiser-paylocity][]) [#1946][] +- [IMPROVEMENT] Ability to clear feature data storage using `clearAllData` API. See [#1940][] # 2.14.1 / 09-07-2024 @@ -721,6 +722,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1938]: https://github.com/DataDog/dd-sdk-ios/pull/1938 [#1947]: https://github.com/DataDog/dd-sdk-ios/pull/1947 [#1948]: https://github.com/DataDog/dd-sdk-ios/pull/1948 +[#1940]: https://github.com/DataDog/dd-sdk-ios/pull/1940 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift index c5aa6d348f..750dda7166 100644 --- a/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift +++ b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift @@ -9,7 +9,7 @@ import DatadogInternal /// A concrete implementation of the `DataStore` protocol using file storage. internal final class FeatureDataStore: DataStore { - private enum Constants { + enum Constants { /// The version of this data store implementation. /// If a breaking change is introduced to the format of managed files, the version must be upgraded and old data should be deleted. static let dataStoreVersion = 1 @@ -35,7 +35,7 @@ internal final class FeatureDataStore: DataStore { ) { self.feature = feature self.coreDirectory = directory - self.directoryPath = "\(Constants.dataStoreVersion)/" + feature + self.directoryPath = coreDirectory.getDataStorePath(forFeatureNamed: feature) self.queue = queue self.telemetry = telemetry } @@ -87,6 +87,18 @@ internal final class FeatureDataStore: DataStore { } } + func clearAllData() { + queue.async { + do { + let directory = try self.coreDirectory.coreDirectory.subdirectory(path: self.directoryPath) + try directory.deleteAllFiles() + } catch let error { + DD.logger.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: error) + self.telemetry.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: DDError(error: error)) + } + } + } + // MARK: - Persistence private func write(data: Data, forKey key: String, version: DataStoreKeyVersion) throws { diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 9f8352545f..bbae88954f 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -53,10 +53,7 @@ internal final class DatadogCore { /// Registry for Features. @ReadWriteLock - private(set) var stores: [String: ( - storage: FeatureStorage, - upload: FeatureUpload - )] = [:] + private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:] /// Registry for Features. @ReadWriteLock @@ -171,6 +168,7 @@ internal final class DatadogCore { /// Clears all data that has not already yet been uploaded Datadog servers. func clearAllData() { allStorages.forEach { $0.clearAllData() } + allDataStores.forEach { $0.clearAllData() } } /// Adds a message receiver to the bus. @@ -197,6 +195,13 @@ internal final class DatadogCore { stores.values.map { $0.upload } } + private var allDataStores: [DataStore] { + features.values.compactMap { feature in + let featureType = type(of: feature) as DatadogFeature.Type + return scope(for: featureType).dataStore + } + } + /// Awaits completion of all asynchronous operations, forces uploads (without retrying) and deinitializes /// this instance of the SDK. It **blocks the caller thread**. /// diff --git a/DatadogCore/Sources/Core/Storage/Directories.swift b/DatadogCore/Sources/Core/Storage/Directories.swift index 22282515cb..6954f189c4 100644 --- a/DatadogCore/Sources/Core/Storage/Directories.swift +++ b/DatadogCore/Sources/Core/Storage/Directories.swift @@ -35,11 +35,22 @@ internal struct CoreDirectory { authorized: try coreDirectory.createSubdirectory(path: "\(name)/v2") ) } + + /// Obtains the path to the data store for given Feature. + /// + /// Note: `FeatureDataStore` directory is created on-demand which may happen before `FeatureDirectories` are created. + /// Hence, this method only returns the path and let the caller decide if the directory should be created. + /// + /// - Parameter name: The given Feature name. + /// - Returns: The path to the data store for given Feature. + func getDataStorePath(forFeatureNamed name: String) -> String { + return "\(FeatureDataStore.Constants.dataStoreVersion)/" + name + } } internal extension CoreDirectory { /// Creates the core directory. - /// + /// /// - Parameters: /// - osDirectory: the root OS directory (`/Library/Caches`) to create core directory inside. /// - instanceName: The core instance name. diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift index e3498d498e..e4ad03dfda 100644 --- a/DatadogCore/Sources/Core/Storage/Files/File.swift +++ b/DatadogCore/Sources/Core/Storage/Files/File.swift @@ -48,7 +48,7 @@ private enum FileError: Error { /// An immutable `struct` designed to provide optimized and thread safe interface for file manipulation. /// It doesn't own the file, which means the file presence is not guaranteed - the file can be deleted by OS at any time (e.g. due to memory pressure). -internal struct File: WritableFile, ReadableFile, FileProtocol { +internal struct File: WritableFile, ReadableFile, FileProtocol, Equatable { let url: URL let name: String diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index d1eb48fb65..221d3f0232 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -336,7 +336,16 @@ class DatadogTests: XCTestCase { try core.directory.getFeatureDirectories(forFeatureNamed: "tracing"), ] - let allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] } + let scope = core.scope(for: TraceFeature.self) + scope.dataStore.setValue("foo".data(using: .utf8)!, forKey: "bar") + + // Wait for async clear completion in all features: + core.readWriteQueue.sync {} + let tracingDataStoreDir = try core.directory.coreDirectory.subdirectory(path: core.directory.getDataStorePath(forFeatureNamed: "tracing")) + XCTAssertTrue(tracingDataStoreDir.hasFile(named: "bar")) + + var allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] } + allDirectories.append(.init(url: tracingDataStoreDir.url)) try allDirectories.forEach { directory in _ = try directory.createFile(named: .mockRandom()) } // When @@ -346,8 +355,11 @@ class DatadogTests: XCTestCase { core.readWriteQueue.sync {} // Then - let newNumberOfFiles = try allDirectories.reduce(0, { acc, nextDirectory in return try acc + nextDirectory.files().count }) - XCTAssertEqual(newNumberOfFiles, 0, "All files must be removed") + let files: [File] = allDirectories.reduce([], { acc, nextDirectory in + let next = try? nextDirectory.files() + return acc + (next ?? []) + }) + XCTAssertEqual(files, [], "All files must be removed") Datadog.flushAndDeinitialize() } diff --git a/DatadogInternal/Sources/DataStore/DataStore.swift b/DatadogInternal/Sources/DataStore/DataStore.swift index 4d9b170d9f..e437ac639b 100644 --- a/DatadogInternal/Sources/DataStore/DataStore.swift +++ b/DatadogInternal/Sources/DataStore/DataStore.swift @@ -61,6 +61,12 @@ public protocol DataStore { /// /// - Parameter key: The unique identifier for the value to be deleted. Must be a valid file name, as it will be persisted in files. func removeValue(forKey key: String) + + /// Clears all data that has not already yet been uploaded Datadog servers. + /// + /// Note: This may impact the SDK's ability to detect App Hangs and Watchdog Terminations + /// or other features that rely on data persisted in the data store. + func clearAllData() } public extension DataStore { @@ -83,4 +89,6 @@ public struct NOPDataStore: DataStore { public func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) {} /// no-op public func removeValue(forKey key: String) {} + /// no-op + public func clearAllData() {} } diff --git a/TestUtilities/Mocks/DataStoreMock.swift b/TestUtilities/Mocks/DataStoreMock.swift index 212b9d51ea..77a2fd6243 100644 --- a/TestUtilities/Mocks/DataStoreMock.swift +++ b/TestUtilities/Mocks/DataStoreMock.swift @@ -18,15 +18,19 @@ public class DataStoreMock: DataStore { public func setValue(_ value: Data, forKey key: String, version: DataStoreKeyVersion) { storage[key] = .value(value, version) } - + public func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) { callback(storage[key] ?? .noValue) } - + public func removeValue(forKey key: String) { storage[key] = nil } - + + public func clearAllData() { + storage.removeAll() + } + // MARK: - Side Effects Observation public func value(forKey key: String) -> DataStoreValueResult? {