diff --git a/CHANGELOG.md b/CHANGELOG.md index a67e511c7c..5301103f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [FEATURE] Support WebView recording in Session Replay. See [#1776][] - [IMPROVEMENT] Add `isInitialized` and `stopInstance` methods to ObjC API. See [#1800][] - [IMPROVEMENT] Add `addUserExtraInfo` method to ObjC API. See [#1799][] +- [FIX] Add background upload capability to extensions in order to mitigate `0xdead10cc` crash. See [#1803][] # 2.10.1 / 02-05-2024 @@ -655,6 +656,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1798]: https://github.com/DataDog/dd-sdk-ios/pull/1798 [#1776]: https://github.com/DataDog/dd-sdk-ios/pull/1776 [#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 +[#1803]: https://github.com/DataDog/dd-sdk-ios/pull/1803 [#1807]: https://github.com/DataDog/dd-sdk-ios/pull/1807 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 3a2ed98c4b..407bcaca8e 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -592,8 +592,10 @@ A7B932FC2B1F6A0A00AE6477 /* SRDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */; }; A7B932FD2B1F6A0A00AE6477 /* EnrichedResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */; }; A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */; }; - A7C816AB2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; - A7C816AC2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */; }; + A7CA21802BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */; }; + A7CA21812BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */; }; + A7CA21832BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */; }; + A7CA21842BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */; }; A7D9528A2B28BD94004C79B1 /* ResourceProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */; }; A7D9528C2B28C18D004C79B1 /* ResourceProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */; }; A7DA18042AB0C91200F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; @@ -2574,7 +2576,8 @@ A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRDataModels.swift; sourceTree = ""; }; A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedResource.swift; sourceTree = ""; }; A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SRDataModels+UIKit.swift"; sourceTree = ""; }; - A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; + A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; + A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorSpy.swift; sourceTree = ""; }; A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorTests.swift; sourceTree = ""; }; A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; @@ -4127,6 +4130,8 @@ 61133C2E2423990D00786299 /* Upload */ = { isa = PBXGroup; children = ( + A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */, + A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */, 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */, 61133C302423990D00786299 /* DataUploadConditionsTests.swift */, 61133C312423990D00786299 /* DataUploadDelayTests.swift */, @@ -4134,7 +4139,6 @@ 61133C322423990D00786299 /* DataUploaderTests.swift */, 61133C342423990D00786299 /* URLSessionClientTests.swift */, 61133C332423990D00786299 /* RequestBuilderTests.swift */, - A7C816AA2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift */, ); path = Upload; sourceTree = ""; @@ -7723,7 +7727,6 @@ 61B8BA91281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, 614798962A459AA80095CB02 /* DDTraceTests.swift in Sources */, D25085102976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, - A7C816AB2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */, 6167E6DD2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, 612C13D02AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */, 61A1A44929643254007909E7 /* DatadogCoreProxy.swift in Sources */, @@ -7832,8 +7835,10 @@ D2B3F052282E827700C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, D20605B92875729E0047275C /* ContextValuePublisherMock.swift in Sources */, D24C9C4D29A7BA3F002057CF /* LogsMocks.swift in Sources */, + A7CA21832BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */, 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */, 6167E7062B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift in Sources */, + A7CA21802BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */, D20605C52875895E0047275C /* KronosClockMock.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, 6134CDB12A691E850061CCD9 /* CoreMetricsTests.swift in Sources */, @@ -8933,7 +8938,6 @@ D24C9C7229A7D57A002057CF /* DirectoriesMock.swift in Sources */, 61DA8CB3286215DE0074A606 /* CryptographyTests.swift in Sources */, D2CB6F0427C520D400A62B57 /* DDTracerTests.swift in Sources */, - A7C816AC2A98CEBA00BF097B /* UIKitBackgroundTaskCoordinatorTests.swift in Sources */, D24C9C6129A7CB0C002057CF /* DatadogLogsFeatureTests.swift in Sources */, D29A9FCF29DDC4BC005C54A4 /* RUMFeatureMocks.swift in Sources */, D22743DE29DEB8B5001A7EF9 /* VitalInfoSamplerTests.swift in Sources */, @@ -8977,6 +8981,7 @@ D2CB6F3227C520D400A62B57 /* JSONDataMatcher.swift in Sources */, 6136CB4B2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */, D25085112976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, + A7CA21842BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */, D2CB6F3327C520D400A62B57 /* FilesOrchestratorTests.swift in Sources */, D2FB1258292E0F10005B13F8 /* TrackingConsentPublisherTests.swift in Sources */, D2CB6F3B27C520D400A62B57 /* NSURLSessionBridge.m in Sources */, @@ -8997,6 +9002,7 @@ D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */, D2CB6F4D27C520D400A62B57 /* DataUploadStatusTests.swift in Sources */, D2CB6F4F27C520D400A62B57 /* RetryingTests.swift in Sources */, + A7CA21812BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */, D2CB6F5027C520D400A62B57 /* DDDatadogTests.swift in Sources */, D2B3F0452823EE8400C2B5EE /* TLVBlockTests.swift in Sources */, D2CB6F5327C520D400A62B57 /* DirectoryTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 7e4e98dfe3..98c80e516d 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -68,6 +68,9 @@ internal final class DatadogCore { /// Flag defining if background tasks are enabled. internal let backgroundTasksEnabled: Bool + /// Flag defining if the SDK is run from an extension. + internal let isRunFromExtension: Bool + /// Maximum number of batches per upload. internal let maxBatchesPerUpload: Int @@ -92,7 +95,8 @@ internal final class DatadogCore { contextProvider: DatadogContextProvider, applicationVersion: String, maxBatchesPerUpload: Int, - backgroundTasksEnabled: Bool + backgroundTasksEnabled: Bool, + isRunFromExtension: Bool = false ) { self.directory = directory self.dateProvider = dateProvider @@ -102,6 +106,7 @@ internal final class DatadogCore { self.contextProvider = contextProvider self.maxBatchesPerUpload = maxBatchesPerUpload self.backgroundTasksEnabled = backgroundTasksEnabled + self.isRunFromExtension = isRunFromExtension self.applicationVersionPublisher = ApplicationVersionPublisher(version: applicationVersion) self.consentPublisher = TrackingConsentPublisher(consent: initialConsent) @@ -258,6 +263,7 @@ extension DatadogCore: DatadogCoreProtocol { performance: performancePreset, backgroundTasksEnabled: backgroundTasksEnabled, maxBatchesPerUpload: maxBatchesPerUpload, + isRunFromExtension: isRunFromExtension, telemetry: telemetry ) diff --git a/DatadogCore/Sources/Core/Storage/Directories.swift b/DatadogCore/Sources/Core/Storage/Directories.swift index 6451020799..22282515cb 100644 --- a/DatadogCore/Sources/Core/Storage/Directories.swift +++ b/DatadogCore/Sources/Core/Storage/Directories.swift @@ -42,10 +42,10 @@ internal extension CoreDirectory { /// /// - Parameters: /// - osDirectory: the root OS directory (`/Library/Caches`) to create core directory inside. - /// - instancenName: The core instance name. + /// - instanceName: The core instance name. /// - site: The cor instance site. - init(in osDirectory: Directory, instancenName: String, site: DatadogSite) throws { - let sdkInstanceUUID = sha256("\(instancenName)\(site)") + init(in osDirectory: Directory, instanceName: String, site: DatadogSite) throws { + let sdkInstanceUUID = sha256("\(instanceName)\(site)") let path = "com.datadoghq/v2/\(sdkInstanceUUID)" self.init( diff --git a/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift index 561d70d61d..90b1adbabb 100644 --- a/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift +++ b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift @@ -28,7 +28,7 @@ internal protocol UIKitAppBackgroundTaskCoordinator { extension UIApplication: UIKitAppBackgroundTaskCoordinator {} -internal class UIKitBackgroundTaskCoordinator: BackgroundTaskCoordinator { +internal class AppBackgroundTaskCoordinator: BackgroundTaskCoordinator { private let app: UIKitAppBackgroundTaskCoordinator? @ReadWriteLock @@ -60,4 +60,38 @@ internal class UIKitBackgroundTaskCoordinator: BackgroundTaskCoordinator { self.currentTaskId = nil } } + +/// Bridge protocol that matches `UIApplication` interface for background tasks. Allows easier testablity. +internal protocol ProcessInfoActivityCoordinator { + func beginActivity(options: ProcessInfo.ActivityOptions, reason: String) -> any NSObjectProtocol + func endActivity(_ activity: any NSObjectProtocol) +} + +extension ProcessInfo: ProcessInfoActivityCoordinator {} + +internal class ExtensionBackgroundTaskCoordinator: BackgroundTaskCoordinator { + private let processInfo: ProcessInfoActivityCoordinator + + @ReadWriteLock + private var currentActivity: NSObjectProtocol? + + internal init( + processInfo: ProcessInfoActivityCoordinator = ProcessInfo() + ) { + self.processInfo = processInfo + } + + internal func beginBackgroundTask() { + endBackgroundTask() + currentActivity = processInfo.beginActivity(options: [.background], reason: "Datadog SDK background upload") + } + + internal func endBackgroundTask() { + guard let currentActivity = currentActivity else { + return + } + processInfo.endActivity(currentActivity) + self.currentActivity = nil + } +} #endif diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift index 6e86010343..dcdd25d1a8 100644 --- a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift @@ -20,6 +20,7 @@ internal struct FeatureUpload { performance: PerformancePreset, backgroundTasksEnabled: Bool, maxBatchesPerUpload: Int, + isRunFromExtension: Bool, telemetry: Telemetry ) { let uploadQueue = DispatchQueue( @@ -34,9 +35,15 @@ internal struct FeatureUpload { ) #if canImport(UIKit) - let backgroundTaskCoordinator = backgroundTasksEnabled - ? UIKitBackgroundTaskCoordinator() - : nil + let backgroundTaskCoordinator: BackgroundTaskCoordinator? + switch (backgroundTasksEnabled, isRunFromExtension) { + case (true, false): + backgroundTaskCoordinator = AppBackgroundTaskCoordinator() + case (true, true): + backgroundTaskCoordinator = ExtensionBackgroundTaskCoordinator() + case (false, _): + backgroundTaskCoordinator = nil + } #else let backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil #endif diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 4cb14c1dd5..d7f32e57b4 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -430,12 +430,13 @@ public enum Datadog { uploadFrequency: debug ? .frequent : configuration.uploadFrequency, bundleType: bundleType ) + let isRunFromExtension = bundleType == .iOSAppExtension // Set default `DatadogCore`: let core = DatadogCore( directory: try CoreDirectory( in: configuration.systemDirectory(), - instancenName: instanceName, + instanceName: instanceName, site: configuration.site ), dateProvider: configuration.dateProvider, @@ -466,7 +467,8 @@ public enum Datadog { ), applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, - backgroundTasksEnabled: configuration.backgroundTasksEnabled + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + isRunFromExtension: isRunFromExtension ) core.telemetry.configuration( diff --git a/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift b/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift index 42a110fd1d..3cb2b68ac4 100644 --- a/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift +++ b/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift @@ -43,7 +43,7 @@ class DirectoriesTests: XCTestCase { let coreDirectories = try fixtures.map { instancenName, site, _ in try CoreDirectory( in: directory, - instancenName: instancenName, + instanceName: instancenName, site: site ) } @@ -65,7 +65,7 @@ class DirectoriesTests: XCTestCase { let coreDirectories = try (0..<50).map { index in try CoreDirectory( in: directory, - instancenName: .mockRandom(among: .alphanumerics, length: 31) + "\(index)", + instanceName: .mockRandom(among: .alphanumerics, length: 31) + "\(index)", site: .mockRandom() ) } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/UIKitBackgroundTaskCoordinatorTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift similarity index 92% rename from DatadogCore/Tests/Datadog/Core/Upload/UIKitBackgroundTaskCoordinatorTests.swift rename to DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift index c827f48b67..49d358f0ac 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/UIKitBackgroundTaskCoordinatorTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift @@ -8,14 +8,14 @@ import XCTest import DatadogInternal @testable import DatadogCore -class UIKitBackgroundTaskCoordinatorTests: XCTestCase { +class AppBackgroundTaskCoordinatorTests: XCTestCase { var appSpy: AppSpy? - var coordinator: UIKitBackgroundTaskCoordinator? + var coordinator: AppBackgroundTaskCoordinator? override func setUp() { super.setUp() appSpy = AppSpy() - coordinator = UIKitBackgroundTaskCoordinator( + coordinator = AppBackgroundTaskCoordinator( app: appSpy ) } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift new file mode 100644 index 0000000000..ca31127a20 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift @@ -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 XCTest +import DatadogInternal +@testable import DatadogCore + +class ExtensionBackgroundTaskCoordinatorTests: XCTestCase { + var processInfoSpy: ProcessInfoSpy! // swiftlint:disable:this implicitly_unwrapped_optional + var coordinator: ExtensionBackgroundTaskCoordinator! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + processInfoSpy = ProcessInfoSpy() + coordinator = ExtensionBackgroundTaskCoordinator( + processInfo: processInfoSpy + ) + } + + func testBeginBackgroundTask() { + coordinator?.beginBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, false) + } + + func testEndBackgroundTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.endBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, true) + } + + func testEndBackgroundTaskNotCalledWhenNotBegan() throws { + coordinator?.endBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, false) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, false) + } + + func testBeginEndsPreviousTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.beginBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, true) + } +} + +class ProcessInfoSpy: ProcessInfoActivityCoordinator { + var beginBackgroundTaskCalled = false + var endBackgroundTaskCalled = false + + func beginActivity(options: ProcessInfo.ActivityOptions, reason: String) -> any NSObjectProtocol { + beginBackgroundTaskCalled = true + return NSObject() + } + + func endActivity(_ activity: any NSObjectProtocol) { + endBackgroundTaskCalled = true + } +}