diff --git a/.github/workflows/build-example-apps.yml b/.github/workflows/build-example-apps.yml index 27b53575..a77a7c5f 100644 --- a/.github/workflows/build-example-apps.yml +++ b/.github/workflows/build-example-apps.yml @@ -9,14 +9,14 @@ jobs: build-brandgame-app: name: Build BrandGame App timeout-minutes: 20 - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: - xcode_version: ["15.1"] + xcode_version: ["15.4"] steps: - name: Select Xcode - # See https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md + # See https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md run: | sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode_version }}.app xcodebuild -version @@ -46,14 +46,14 @@ jobs: build-demoobjc-app: name: Build Objc Demo App timeout-minutes: 20 - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: - xcode_version: ["15.1"] + xcode_version: ["15.4"] steps: - name: Select Xcode - # See https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md + # See https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md run: | sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode_version }}.app xcodebuild -version diff --git a/.github/workflows/build-platforms.yml b/.github/workflows/build-platforms.yml index 4c1da9af..ebc8c00d 100644 --- a/.github/workflows/build-platforms.yml +++ b/.github/workflows/build-platforms.yml @@ -12,15 +12,15 @@ jobs: build: name: Build ${{ matrix.platform_args }} - Xcode ${{ matrix.xcode_version }} timeout-minutes: 30 - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: - platform_args: ["iOS macOS tvOS"] # using a single string will run platforms in parallel - xcode_version: ["15.1"] + platform_args: ["iOS tvOS"] # using a single string will run platforms in parallel + xcode_version: ["15.4"] steps: - name: Select Xcode - # See https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md + # See https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md run: | sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode_version }}.app xcodebuild -version diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 2d3a2e96..0b234400 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -103,7 +103,7 @@ jobs: build_release_candidate: name: Bump Version and Build Release - runs-on: macos-13 + runs-on: macos-14 timeout-minutes: 60 needs: - extractor @@ -157,7 +157,7 @@ jobs: fi - name: Select Xcode 15 - run: sudo xcode-select -switch /Applications/Xcode_15.1.app + run: sudo xcode-select -switch /Applications/Xcode_15.4.app - name: Install Mise run: | @@ -187,6 +187,12 @@ jobs: with: name: Embrace-Universal Build Artifacts path: xcframeworks.zip + + - name: Tag the release candidate version + if: env.IS_PRODUCTION_READY == 'false' + run: | + git tag $RC_VERSION + git push origin $RC_VERSION archive_cocoapods_artifacts: name: Archive Cocoapods Artifacts @@ -275,7 +281,7 @@ jobs: if gh release view --repo embrace-io/embrace-apple-sdk $RC_VERSION > /dev/null 2>&1; then if [ "$IS_PRODUCTION_READY" == "false" ]; then echo "Release $RC_VERSION already exists; editing..." - gh release upload $RC_VERSION cocoapods/embrace_$RC_VERSION.zip --clobber --repo embrace-io/embrace-apple-sdk + gh release upload $RC_VERSION cocoapods/embrace_$RC_VERSION.zip --clobber --repo embrace-io/embrace-apple-sdk --verify-tag else echo "Cannot update a production release" exit 1 @@ -286,12 +292,12 @@ jobs: if [ "$IS_PRODUCTION_READY" == "false" ]; then PRERELEASE_FLAG="--prerelease" fi - gh release create $RC_VERSION cocoapods/embrace_$RC_VERSION.zip --title "$RC_VERSION" $PRERELEASE_FLAG --repo embrace-io/embrace-apple-sdk + gh release create $RC_VERSION cocoapods/embrace_$RC_VERSION.zip --title "$RC_VERSION" $PRERELEASE_FLAG --repo embrace-io/embrace-apple-sdk --verify-tag fi push_podspec: name: Push Podspec to Cocoapods - runs-on: macos-13 + runs-on: macos-14 timeout-minutes: 10 needs: - extractor @@ -304,6 +310,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: + ref: ${{ needs.extractor.outputs.rc_version }} fetch-depth: 0 path: embrace-apple-sdk diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme index 152af8f3..b79ecff6 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme @@ -629,6 +629,16 @@ ReferencedContainer = "container:"> + + + + "Packages" --> "Reset package caches" +To test that your changes fixed the auth issue, attempt to fetch the dependencies with "File" -> "Packages" --> "Reset package caches". ### WatchOS Support > [!WARNING] @@ -211,13 +211,14 @@ To test if your auth changes fixed things, attempt to fetch the dependencies wit ## Support -We appreciate any feedback you have on the SDK and the APIs that is provides. To contribute to this project please see our [Contribution Guidelines](https://github.com/embrace-io/embrace-apple-sdk/blob/main/CONTRIBUTING.md) there you'll be able to submit a feature request, create a bug report, or submit a pull request. +We appreciate any feedback you have on the SDK and the APIs that it provides. -For urgent matters (such as outages) or issues concerning the Embrace service or UI reach out in our [Community Slack](https://join.slack.com/t/embraceio-community/shared_invite/zt-ywr4jhzp-DLROX0ndN9a0soHMf6Ksow) for direct, faster assistance. +To contribute to this project please see our [Contribution Guidelines](https://github.com/embrace-io/embrace-apple-sdk/blob/main/CONTRIBUTING.md). After completing the Individual Contributor License Agreement (CLA), you'll be able to submit a feature request, create a bug report, or submit a pull request. + +For urgent matters (such as outages) or issues concerning the Embrace service or UI, reach out in our [Community Slack](community.embrace.io) for direct, faster assistance. ## License [![Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-orange)](./LICENSE.txt) -Embrace Apple SDK is published under the Apache-2.0 license. +Embrace Apple SDK is published under the Apache-2.0 license. -[![Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-orange)](./LICENSE.txt) diff --git a/Sources/EmbraceCaptureService/CaptureService.swift b/Sources/EmbraceCaptureService/CaptureService.swift index f4de9ba2..6e318fbd 100644 --- a/Sources/EmbraceCaptureService/CaptureService.swift +++ b/Sources/EmbraceCaptureService/CaptureService.swift @@ -16,7 +16,7 @@ import OpenTelemetryApi /// Multiple `CaptureServices` can run at the same time and be in charge of handling /// different types of data. /// -/// This base class provides the necessary functionallity and structure that should be used +/// This base class provides the necessary functionality and structure that should be used /// by all capture services. @objc(EMBCaptureService) open class CaptureService: NSObject { @@ -103,7 +103,7 @@ extension CaptureService { /// Use this method to generate events with the capture service. /// - Parameters: /// - event: `RecordingSpanEvent` instance to add. - /// - Returns: Boolean indicating if the event was succesfully added. If the capture service is not active, this method always returns false. + /// - Returns: Boolean indicating if the event was successfully added. If the capture service is not active, this method always returns false. @discardableResult public func add(event: RecordingSpanEvent) -> Bool { guard state == .active else { @@ -118,7 +118,7 @@ extension CaptureService { /// Use this method to generate events with the capture service. /// - Parameters: /// - events: Array of `RecordingSpanEvents` to add. - /// - Returns: Boolean indicating if the events were succesfully added. If the capture service is not active, this method always returns false. + /// - Returns: Boolean indicating if the events were successfully added. If the capture service is not active, this method always returns false. @discardableResult public func add(events: [RecordingSpanEvent]) -> Bool { guard state == .active else { diff --git a/Sources/EmbraceCommonInternal/EmbraceMeta.swift b/Sources/EmbraceCommonInternal/EmbraceMeta.swift index 4168ad37..2009b085 100644 --- a/Sources/EmbraceCommonInternal/EmbraceMeta.swift +++ b/Sources/EmbraceCommonInternal/EmbraceMeta.swift @@ -6,5 +6,5 @@ // Do not edit this file manually public class EmbraceMeta { - public static let sdkVersion = "6.4.0" + public static let sdkVersion = "6.4.2" } diff --git a/Sources/EmbraceCommonInternal/Identifiers/DeviceIdentifier.swift b/Sources/EmbraceCommonInternal/Identifiers/DeviceIdentifier.swift index ad327709..ca6a2d7d 100644 --- a/Sources/EmbraceCommonInternal/Identifiers/DeviceIdentifier.swift +++ b/Sources/EmbraceCommonInternal/Identifiers/DeviceIdentifier.swift @@ -23,3 +23,19 @@ public struct DeviceIdentifier: Equatable { public var hex: String { value.withoutHyphen } } + +extension DeviceIdentifier { + /// Returns the integer value of the device identifier using the number of digits from the suffix + /// - Parameters: + /// - digitCount: The number of digits to use for the deviceId calculation + public func intValue(digitCount: UInt) -> UInt64 { + var deviceIdHexValue: UInt64 = UInt64.max // defaults to everything disabled + + let hexValue = hex + if hexValue.count >= digitCount { + deviceIdHexValue = UInt64.init(hexValue.suffix(Int(digitCount)), radix: 16) ?? .max + } + + return deviceIdHexValue + } +} diff --git a/Sources/EmbraceCommonInternal/Protocols/Swizzlable.swift b/Sources/EmbraceCommonInternal/Protocols/Swizzlable.swift index bc78446f..2bd59a86 100644 --- a/Sources/EmbraceCommonInternal/Protocols/Swizzlable.swift +++ b/Sources/EmbraceCommonInternal/Protocols/Swizzlable.swift @@ -53,9 +53,9 @@ public extension Swizzlable { private func swizzle(method: Method, withBlock block: (ImplementationType) -> BlockImplementationType) { let originalImplementation = method_getImplementation(method) saveInCache(originalImplementation: originalImplementation, forMethod: method) - let originalTypifiedImplmentation = unsafeBitCast(originalImplementation, + let originalTypifiedImplementation = unsafeBitCast(originalImplementation, to: ImplementationType.self) - let newImplementationBlock: BlockImplementationType = block(originalTypifiedImplmentation) + let newImplementationBlock: BlockImplementationType = block(originalTypifiedImplementation) let newImplementation = imp_implementationWithBlock(newImplementationBlock) method_setImplementation(method, newImplementation) } diff --git a/Sources/EmbraceCommonInternal/UnfairLock.swift b/Sources/EmbraceCommonInternal/UnfairLock.swift index 707a12ea..36116838 100644 --- a/Sources/EmbraceCommonInternal/UnfairLock.swift +++ b/Sources/EmbraceCommonInternal/UnfairLock.swift @@ -8,7 +8,7 @@ import Foundation /// crashes due to how Swift's memory model works. /// /// For more information: -/// - Swift law of exlusivity: https://github.com/apple/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md +/// - Swift law of exclusivity: https://github.com/apple/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md final public class UnfairLock { private var _lock: UnsafeMutablePointer diff --git a/Sources/EmbraceConfigInternal/EmbraceConfig+Options.swift b/Sources/EmbraceConfigInternal/EmbraceConfig+Options.swift index f6abc52c..5dabe4d9 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfig+Options.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfig+Options.swift @@ -3,44 +3,16 @@ // import Foundation +import EmbraceCommonInternal public extension EmbraceConfig { class Options { - let apiBaseUrl: String - let queue: DispatchQueue - - let appId: String - let deviceId: String - let osVersion: String - let sdkVersion: String - let appVersion: String - let userAgent: String - let minimumUpdateInterval: TimeInterval - let urlSessionConfiguration: URLSessionConfiguration public init( - apiBaseUrl: String, - queue: DispatchQueue, - appId: String, - deviceId: String, - osVersion: String, - sdkVersion: String, - appVersion: String, - userAgent: String, - minimumUpdateInterval: TimeInterval = 60 * 60, - urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default) { - - self.apiBaseUrl = apiBaseUrl - self.queue = queue - self.appId = appId - self.deviceId = deviceId - self.osVersion = osVersion - self.sdkVersion = sdkVersion - self.appVersion = appVersion - self.userAgent = userAgent + minimumUpdateInterval: TimeInterval = 60 * 60 + ) { self.minimumUpdateInterval = minimumUpdateInterval - self.urlSessionConfiguration = urlSessionConfiguration } } } diff --git a/Sources/EmbraceConfigInternal/EmbraceConfig.swift b/Sources/EmbraceConfigInternal/EmbraceConfig.swift index 69be0e11..54b22433 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfig.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfig.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceCommonInternal +import EmbraceConfiguration public extension Notification.Name { static let embraceConfigUpdated = Notification.Name("embraceConfigUpdated") @@ -15,32 +16,23 @@ public class EmbraceConfig { let logger: InternalLogger let notificationCenter: NotificationCenter - let deviceIdUsedDigits = 6 - var deviceIdHexValue: UInt64 = UInt64.max // defaults to everything disabled - - @ThreadSafe var payload: RemoteConfigPayload = RemoteConfigPayload() - let fetcher: RemoteConfigFetcher - - @ThreadSafe private(set) var updating = false @ThreadSafe private var lastUpdateTime: TimeInterval = Date(timeIntervalSince1970: 0).timeIntervalSince1970 - public var onUpdate: (() -> Void)? + let configurable: EmbraceConfigurable - public init(options: Options, notificationCenter: NotificationCenter, logger: InternalLogger) { + public init( + configurable: EmbraceConfigurable, + options: Options, + notificationCenter: NotificationCenter, + logger: InternalLogger + ) { self.options = options self.notificationCenter = notificationCenter self.logger = logger + self.configurable = configurable - fetcher = RemoteConfigFetcher(options: options, logger: logger) update() - // get hex value of the last 6 digits of the device id - if options.deviceId.count >= deviceIdUsedDigits { - let hexString = String(options.deviceId.suffix(deviceIdUsedDigits)) - let scanner = Scanner(string: hexString) - scanner.scanHexInt64(&deviceIdHexValue) - } - // using hardcoded string to avoid reference to UIApplication reference NotificationCenter.default.addObserver( self, @@ -54,43 +46,6 @@ public class EmbraceConfig { NotificationCenter.default.removeObserver(self) } - // MARK: - Configs - public var isSDKEnabled: Bool { - return isEnabled(threshold: payload.sdkEnabledThreshold) - } - - public var isBackgroundSessionEnabled: Bool { - return isEnabled(threshold: payload.backgroundSessionThreshold) - } - - public var isNetworkSpansForwardingEnabled: Bool { - return isEnabled(threshold: payload.networkSpansForwardingThreshold) - } - - public var internalLogsTraceLimit: Int { - return payload.internalLogsTraceLimit - } - - public var internalLogsDebugLimit: Int { - return payload.internalLogsDebugLimit - } - - public var internalLogsInfoLimit: Int { - return payload.internalLogsInfoLimit - } - - public var internalLogsWarningLimit: Int { - return payload.internalLogsWarningLimit - } - - public var internalLogsErrorLimit: Int { - return payload.internalLogsErrorLimit - } - - public var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { - return payload.networkPayloadCaptureRules - } - // MARK: - Update @discardableResult public func updateIfNeeded() -> Bool { @@ -99,47 +54,49 @@ public class EmbraceConfig { } update() + lastUpdateTime = Date().timeIntervalSince1970 return true } public func update() { - guard updating == false else { - return - } - - updating = true - - fetcher.fetch { [weak self] payload in - if let payload = payload { - let previousPayload = self?.payload - - self?.payload = payload - - if previousPayload != payload { - self?.notificationCenter.post(name: .embraceConfigUpdated, object: self) - } - - self?.lastUpdateTime = Date().timeIntervalSince1970 + configurable.update { [weak self] didChange, error in + if let error = error { + self?.logger.error( + "Failed update in EmbraceConfig", + attributes: [ "error.message": error.localizedDescription ] + ) } - self?.updating = false + if didChange { + self?.notificationCenter.post(name: .embraceConfigUpdated, object: self) + } } } - // MARK: - Private - func isEnabled(threshold: Float) -> Bool { - return EmbraceConfig.isEnabled(hexValue: deviceIdHexValue, digits: deviceIdUsedDigits, threshold: threshold) + // MARK: - Notifications + @objc func appDidBecomeActive() { + self.updateIfNeeded() + } +} + +extension EmbraceConfig /* EmbraceConfigurable delegation */ { + public var isSDKEnabled: Bool { + return configurable.isSDKEnabled + } + + public var isBackgroundSessionEnabled: Bool { + return configurable.isBackgroundSessionEnabled } - class func isEnabled(hexValue: UInt64, digits: Int, threshold: Float) -> Bool { - let space = powf(16, Float(digits)) - let result = (Float(hexValue) / space) * 100 + public var isNetworkSpansForwardingEnabled: Bool { + return configurable.isNetworkSpansForwardingEnabled + } - return result < threshold + public var internalLogLimits: InternalLogLimits { + return configurable.internalLogLimits } - // MARK: - Notifications - @objc func appDidBecomeActive() { - self.updateIfNeeded() + public var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { + return configurable.networkPayloadCaptureRules } } diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift new file mode 100644 index 00000000..ba98244a --- /dev/null +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift @@ -0,0 +1,113 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import EmbraceConfiguration + +/// Remote config uses the Embrace Config Service to request config values +public class RemoteConfig { + + // config requests + @ThreadSafe var payload: RemoteConfigPayload + let fetcher: RemoteConfigFetcher + + // threshold values + static let deviceIdUsedDigits: UInt = 6 + let deviceIdHexValue: UInt64 + + @ThreadSafe private(set) var updating = false + + public convenience init( + options: RemoteConfig.Options, + payload: RemoteConfigPayload = RemoteConfigPayload(), + logger: InternalLogger + ) { + self.init(options: options, + fetcher: RemoteConfigFetcher(options: options, logger: logger), + logger: logger) + } + + init( + options: RemoteConfig.Options, + payload: RemoteConfigPayload = RemoteConfigPayload(), + fetcher: RemoteConfigFetcher, + logger: InternalLogger + ) { + self.payload = payload + self.fetcher = fetcher + self.deviceIdHexValue = options.deviceId.intValue(digitCount: Self.deviceIdUsedDigits) + } +} + +extension RemoteConfig: EmbraceConfigurable { + public var isSDKEnabled: Bool { isEnabled(threshold: payload.sdkEnabledThreshold) } + + public var isBackgroundSessionEnabled: Bool { isEnabled(threshold: payload.backgroundSessionThreshold) } + + public var isNetworkSpansForwardingEnabled: Bool { isEnabled(threshold: payload.networkSpansForwardingThreshold) } + + public var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { payload.networkPayloadCaptureRules } + + public var internalLogLimits: InternalLogLimits { + InternalLogLimits( + trace: UInt(max(payload.internalLogsTraceLimit, 0)), + debug: UInt(max(payload.internalLogsDebugLimit, 0)), + info: UInt(max(payload.internalLogsInfoLimit, 0)), + warning: UInt(max(payload.internalLogsWarningLimit, 0)), + error: UInt(max(payload.internalLogsErrorLimit, 0)) + ) + } + + public func update(completion: @escaping (Bool, (any Error)?) -> Void) { + guard updating == false else { + completion(false, nil) + return + } + + updating = true + fetcher.fetch { [weak self] newPayload in + defer { self?.updating = false } + guard let strongSelf = self else { + completion(false, nil) + return + } + + guard let newPayload = newPayload else { + completion(false, nil) + return + } + + let didUpdate = strongSelf.payload != newPayload + strongSelf.payload = newPayload + + completion(didUpdate, nil) + } + } +} + +extension RemoteConfig { + func isEnabled(threshold: Float) -> Bool { + return Self.isEnabled(hexValue: deviceIdHexValue, digits: Self.deviceIdUsedDigits, threshold: threshold) + } + + /// Algorithm to determine if percentage threshold is enabled for the hexValue + /// Given a `hexValue` (derived from DeviceIdentifier to persist across app launches) + /// Determine the max value for the probability `space` by using the number of `digits` (16 ^ `n`) + /// If the `hexValue` is within the `threshold` + /// ``` + /// space = 16^numDigits + /// result = (hexValue / space) * 100.0 < threshold + /// ``` + /// - Parameters: + /// - hexValue: The value to test + /// - digits: The number of digits used to calculate the total space. Must match the number of digits used to determine the hexValue + /// - threshold: The percentage threshold to test against. Values between 0.0 and 100.0 + static func isEnabled(hexValue: UInt64, digits: UInt, threshold: Float) -> Bool { + let space = powf(16, Float(digits)) + let result = (Float(hexValue) / space) * 100 + + return result < threshold + } +} diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift new file mode 100644 index 00000000..6961bcab --- /dev/null +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfig+Options.swift @@ -0,0 +1,44 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal + +public extension RemoteConfig { + struct Options { + let apiBaseUrl: String + let queue: DispatchQueue + + let appId: String + let deviceId: DeviceIdentifier + let osVersion: String + let sdkVersion: String + let appVersion: String + let userAgent: String + + let urlSessionConfiguration: URLSessionConfiguration + + public init( + apiBaseUrl: String, + queue: DispatchQueue, + appId: String, + deviceId: DeviceIdentifier, + osVersion: String, + sdkVersion: String, + appVersion: String, + userAgent: String, + urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default + ) { + self.apiBaseUrl = apiBaseUrl + self.queue = queue + self.appId = appId + self.deviceId = deviceId + self.osVersion = osVersion + self.sdkVersion = sdkVersion + self.appVersion = appVersion + self.userAgent = userAgent + self.urlSessionConfiguration = urlSessionConfiguration + } + } +} diff --git a/Sources/EmbraceConfigInternal/RemoteConfigFetcher.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift similarity index 91% rename from Sources/EmbraceConfigInternal/RemoteConfigFetcher.swift rename to Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift index 8d0ab302..ff155308 100644 --- a/Sources/EmbraceConfigInternal/RemoteConfigFetcher.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcher.swift @@ -9,12 +9,12 @@ class RemoteConfigFetcher { static let routePath = "/v2/config" - let options: EmbraceConfig.Options + let options: RemoteConfig.Options let logger: InternalLogger let session: URLSession let operationQueue: OperationQueue - init(options: EmbraceConfig.Options, logger: InternalLogger) { + public init(options: RemoteConfig.Options, logger: InternalLogger) { self.options = options self.logger = logger @@ -27,7 +27,7 @@ class RemoteConfigFetcher { ) } - public func fetch(completion: @escaping (RemoteConfigPayload?) -> Void) { + func fetch(completion: @escaping (RemoteConfigPayload?) -> Void) { guard let request = newRequest() else { completion(nil) return @@ -58,7 +58,7 @@ class RemoteConfigFetcher { do { let payload = try JSONDecoder().decode(RemoteConfigPayload.self, from: data) - self?.logger.info("Succesfully fetched remote config") + self?.logger.info("Successfully fetched remote config") completion(payload) } catch { self?.logger.error("Error decoding remote config:\n\(error.localizedDescription)") @@ -77,7 +77,6 @@ class RemoteConfigFetcher { URLQueryItem(name: "appId", value: options.appId), URLQueryItem(name: "osVersion", value: options.osVersion), URLQueryItem(name: "appVersion", value: options.appVersion), - URLQueryItem(name: "deviceId", value: options.deviceId), URLQueryItem(name: "sdkVersion", value: options.sdkVersion) ] diff --git a/Sources/EmbraceConfigInternal/RemoteConfigPayload.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigPayload.swift similarity index 94% rename from Sources/EmbraceConfigInternal/RemoteConfigPayload.swift rename to Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigPayload.swift index 01c984e5..05c8f005 100644 --- a/Sources/EmbraceConfigInternal/RemoteConfigPayload.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig/RemoteConfigPayload.swift @@ -3,10 +3,11 @@ // import Foundation +import EmbraceConfiguration // swiftlint:disable nesting -struct RemoteConfigPayload: Decodable, Equatable { +public struct RemoteConfigPayload: Decodable, Equatable { var sdkEnabledThreshold: Float var backgroundSessionThreshold: Float var networkSpansForwardingThreshold: Float @@ -28,7 +29,7 @@ struct RemoteConfigPayload: Decodable, Equatable { } case networkSpansForwarding = "network_span_forwarding" - enum NetworkSpansForwardingCodigKeys: String, CodingKey { + enum NetworkSpansForwardingCodingKeys: String, CodingKey { case threshold = "pct_enabled" } @@ -71,12 +72,12 @@ struct RemoteConfigPayload: Decodable, Equatable { // network span forwarding if rootContainer.contains(.networkSpansForwarding) { let networkSpansForwardingContainer = try rootContainer.nestedContainer( - keyedBy: CodingKeys.NetworkSpansForwardingCodigKeys.self, + keyedBy: CodingKeys.NetworkSpansForwardingCodingKeys.self, forKey: .networkSpansForwarding ) networkSpansForwardingThreshold = try networkSpansForwardingContainer.decodeIfPresent( Float.self, - forKey: CodingKeys.NetworkSpansForwardingCodigKeys.threshold + forKey: CodingKeys.NetworkSpansForwardingCodingKeys.threshold ) ?? defaultPayload.networkSpansForwardingThreshold } else { networkSpansForwardingThreshold = defaultPayload.networkSpansForwardingThreshold diff --git a/Sources/EmbraceConfigInternal/NetworkPayloadCaptureRule.swift b/Sources/EmbraceConfigInternal/NetworkPayloadCaptureRule.swift deleted file mode 100644 index 361e5406..00000000 --- a/Sources/EmbraceConfigInternal/NetworkPayloadCaptureRule.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation - -public struct NetworkPayloadCaptureRule: Decodable, Equatable { - - public let id: String - public let urlRegex: String - public let statusCodes: [Int]? - public let methods: [String]? - public let expiration: Double - public let publicKey: String - - public var expirationDate: Date { - return Date(timeIntervalSince1970: expiration) - } - - enum CodingKeys: String, CodingKey { - case id - case urlRegex = "url" - case statusCodes = "status_code" - case methods = "method" - case expiration - case publicKey = "public_key" - } -} diff --git a/Sources/EmbraceConfiguration/EmbraceConfigurable.swift b/Sources/EmbraceConfiguration/EmbraceConfigurable.swift new file mode 100644 index 00000000..3c73447b --- /dev/null +++ b/Sources/EmbraceConfiguration/EmbraceConfigurable.swift @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +/// This protocol is used to add configuration to the runtime of the SDK +/// It is used to configure the ongoing behavior of the SDK +@objc public protocol EmbraceConfigurable { + var isSDKEnabled: Bool { get } + + var isBackgroundSessionEnabled: Bool { get } + + var isNetworkSpansForwardingEnabled: Bool { get } + + var internalLogLimits: InternalLogLimits { get } + + var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { get } + + /// Tell the configurable implementation it should update if possible. + /// - Parameters: + /// - completion: A completion block that takes two parameters (didChange, error). Completion block should pass `true` + /// if the configuration now has different values and `false` if not in the case of an error updating, the completion block should + /// return `false` and an Error object describing the issue. + func update(completion: @escaping (Bool, Error?) -> Void) +} diff --git a/Sources/EmbraceConfiguration/EmbraceConfigurable/DefaultConfig.swift b/Sources/EmbraceConfiguration/EmbraceConfigurable/DefaultConfig.swift new file mode 100644 index 00000000..961e21a5 --- /dev/null +++ b/Sources/EmbraceConfiguration/EmbraceConfigurable/DefaultConfig.swift @@ -0,0 +1,27 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +public class DefaultConfig: EmbraceConfigurable { + public let isSDKEnabled: Bool = true + + public let isBackgroundSessionEnabled: Bool = false + + public let isNetworkSpansForwardingEnabled: Bool = false + + public let internalLogLimits = InternalLogLimits() + + public let networkPayloadCaptureRules = [NetworkPayloadCaptureRule]() + + public func update(completion: (Bool, (any Error)?) -> Void) { + completion(false, nil) + } + + public init() { } +} + +extension EmbraceConfigurable where Self == DefaultConfig { + public static var `default`: EmbraceConfigurable { + return DefaultConfig() + } +} diff --git a/Sources/EmbraceConfiguration/InternalLogLimits.swift b/Sources/EmbraceConfiguration/InternalLogLimits.swift new file mode 100644 index 00000000..2c2d4271 --- /dev/null +++ b/Sources/EmbraceConfiguration/InternalLogLimits.swift @@ -0,0 +1,37 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +/// InternalLogLimits manages limits for the logs the SDK produces about its own operation +/// This is broken into the major log severities so each can be managed +/// These logs will only be emitted if the ``Embrace.logLevel`` +@objc public class InternalLogLimits: NSObject { + public let trace: UInt + public let debug: UInt + public let info: UInt + public let warning: UInt + public let error: UInt + + public init(trace: UInt = 0, debug: UInt = 0, info: UInt = 0, warning: UInt = 0, error: UInt = 3) { + self.trace = trace + self.debug = debug + self.info = info + self.warning = warning + self.error = error + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? Self else { + return false + } + + return + trace == other.trace && + debug == other.debug && + info == other.info && + warning == other.warning && + error == other.error + } +} diff --git a/Sources/EmbraceConfiguration/NetworkPayloadCaptureRule.swift b/Sources/EmbraceConfiguration/NetworkPayloadCaptureRule.swift new file mode 100644 index 00000000..021ec9b0 --- /dev/null +++ b/Sources/EmbraceConfiguration/NetworkPayloadCaptureRule.swift @@ -0,0 +1,52 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +@objc +public final class NetworkPayloadCaptureRule: NSObject { + public let id: String + public let urlRegex: String + public let statusCodes: [Int]? + public let methods: [String]? + public let expiration: Double + public let publicKey: String + + public var expirationDate: Date { + return Date(timeIntervalSince1970: expiration) + } + + init( + id: String, + urlRegex: String, + statusCodes: [Int]?, + methods: [String]?, + expiration: Double, + publicKey: String + ) { + self.id = id + self.urlRegex = urlRegex + self.statusCodes = statusCodes + self.methods = methods + self.expiration = expiration + self.publicKey = publicKey + } +} + +extension NetworkPayloadCaptureRule: Decodable { + enum CodingKeys: String, CodingKey { + case id + case urlRegex = "url" + case statusCodes = "status_code" + case methods = "method" + case expiration + case publicKey = "public_key" + } +} + +extension NetworkPayloadCaptureRule /* Equatable */ { + public static func == (lhs: NetworkPayloadCaptureRule, rhs: NetworkPayloadCaptureRule) -> Bool { + lhs.id == rhs.id + } +} diff --git a/Sources/EmbraceCore/Capture/CaptureServices.swift b/Sources/EmbraceCore/Capture/CaptureServices.swift index 8415b601..c307eb4f 100644 --- a/Sources/EmbraceCore/Capture/CaptureServices.swift +++ b/Sources/EmbraceCore/Capture/CaptureServices.swift @@ -18,7 +18,7 @@ final class CaptureServices { init(options: Embrace.Options, storage: EmbraceStorage?, upload: EmbraceUpload?) throws { // add required capture services - // adn remove duplicates + // and remove duplicates services = CaptureServiceFactory.addRequiredServices(to: options.services.unique) // create context for crash reporter diff --git a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift index fd67f64f..b4bd5a21 100644 --- a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift +++ b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift @@ -8,6 +8,7 @@ import EmbraceCommonInternal import EmbraceOTelInternal import EmbraceStorageInternal import EmbraceSemantics +import EmbraceConfiguration class NetworkPayloadCaptureHandler { @@ -48,6 +49,12 @@ class NetworkPayloadCaptureHandler { ) updateRules(Embrace.client?.config?.networkPayloadCaptureRules) + + // check if a session is already started + if let sessionId = Embrace.client?.currentSessionId() { + active = true + currentSessionId = SessionIdentifier(string: sessionId) + } } deinit { diff --git a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRule.swift b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRule.swift index 8f9c7d52..1378356a 100644 --- a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRule.swift +++ b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRule.swift @@ -3,7 +3,7 @@ // import Foundation -import EmbraceConfigInternal +import EmbraceConfiguration class URLSessionTaskCaptureRule { diff --git a/Sources/EmbraceCore/Capture/Network/Proxy/URLSessionDelegateProxy.swift b/Sources/EmbraceCore/Capture/Network/Proxy/URLSessionDelegateProxy.swift index 7e4bbfa0..cad6e76b 100644 --- a/Sources/EmbraceCore/Capture/Network/Proxy/URLSessionDelegateProxy.swift +++ b/Sources/EmbraceCore/Capture/Network/Proxy/URLSessionDelegateProxy.swift @@ -80,7 +80,7 @@ class URLSessionDelegateProxy: NSObject { URLSessionDelegate.urlSession(_:didBecomeInvalidWithError:) ) // If the `originalDelegate` has implemented the `didBecomeInvalidWithError` we forward them the call to them. - // However, to prevent any kind of leakeage, we clean on end the `originalDelegate` so we don't retain it. + // However, to prevent any kind of leakage, we clean on end the `originalDelegate` so we don't retain it. invokeDelegates(session: session, selector: selector) { (delegate: URLSessionDelegate) in delegate.urlSession?(session, didBecomeInvalidWithError: error) } diff --git a/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService+Options.swift b/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService+Options.swift index e4bfa948..068a9e8e 100644 --- a/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService+Options.swift +++ b/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService+Options.swift @@ -11,7 +11,7 @@ extension URLSessionCaptureService { /// Defines wether or not the Embrace SDK should inject the `traceparent` header into all network requests @objc public let injectTracingHeader: Bool - /// `URLSessionRequestsDataSource` instance that will manipuate all network requests + /// `URLSessionRequestsDataSource` instance that will manipulate all network requests /// before the Embrace SDK captures their data. @objc public let requestsDataSource: URLSessionRequestsDataSource? diff --git a/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService.swift b/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService.swift index 1188052d..e65a8f70 100644 --- a/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService.swift +++ b/Sources/EmbraceCore/Capture/Network/URLSessionCaptureService.swift @@ -102,6 +102,12 @@ struct URLSessionInitWithDelegateSwizzler: URLSessionSwizzler { try swizzleClassMethod { originalImplementation -> BlockImplementationType in return { urlSession, configuration, delegate, queue -> URLSession in let proxiedDelegate = (delegate != nil) ? delegate : EmbraceDummyURLSessionDelegate() + + // check if we support proxying this type of delegate + guard isDelegateSupported(proxiedDelegate) else { + return originalImplementation(urlSession, Self.selector, configuration, delegate, queue) + } + let newDelegate = URLSessionDelegateProxy(originalDelegate: proxiedDelegate, handler: handler) let session = originalImplementation(urlSession, Self.selector, configuration, newDelegate, queue) @@ -116,6 +122,32 @@ struct URLSessionInitWithDelegateSwizzler: URLSessionSwizzler { } } } + + // list of third party URLSessionDelegate implementations that we don't support + // due to issues / crashes out of our control + private let unsupportedDelegates: [String] = [ + + // This type belongs to an internal library used by Firebase which + // incorrectly assumes the type of the URLSession delegate, resulting + // in it calling a method that is not implemented by our proxy. + // + // We can't solve this on our side in a clean way so we'll just not + // capture any requests from this library until the issue is solved + // on their side. + // + // Library: https://github.com/google/gtm-session-fetcher/ + // Issue: https://github.com/google/gtm-session-fetcher/issues/190 + "GTMSessionFetcher" + ] + + func isDelegateSupported(_ delegate: AnyObject?) -> Bool { + guard let delegate = delegate else { + return true + } + + let name = NSStringFromClass(type(of: delegate)) + return unsupportedDelegates.first { name.contains($0) } == nil + } } struct SessionTaskResumeSwizzler: URLSessionSwizzler { diff --git a/Sources/EmbraceCore/Capture/Network/URLSessionRequestsDataSource.swift b/Sources/EmbraceCore/Capture/Network/URLSessionRequestsDataSource.swift index 68f7469b..4c0f9127 100644 --- a/Sources/EmbraceCore/Capture/Network/URLSessionRequestsDataSource.swift +++ b/Sources/EmbraceCore/Capture/Network/URLSessionRequestsDataSource.swift @@ -8,7 +8,7 @@ import Foundation /// captures their data into OTel spans. /// /// Example: -/// This could be useful if you need to obfuscate certains parts of a request path +/// This could be useful if you need to obfuscate certain parts of a request path /// if it contains sensitive data. @objc public protocol URLSessionRequestsDataSource: NSObjectProtocol { @objc func modifiedRequest(for request: URLRequest) -> URLRequest diff --git a/Sources/EmbraceCore/Capture/Network/URLSessionTaskHandler.swift b/Sources/EmbraceCore/Capture/Network/URLSessionTaskHandler.swift index e38fa4f4..d3b7fa8f 100644 --- a/Sources/EmbraceCore/Capture/Network/URLSessionTaskHandler.swift +++ b/Sources/EmbraceCore/Capture/Network/URLSessionTaskHandler.swift @@ -150,7 +150,7 @@ final class DefaultURLSessionTaskHandler: URLSessionTaskHandler { return } - // generate attributes from reponse + // generate attributes from response if let response = task.response as? HTTPURLResponse { span.setAttribute( key: SpanSemantics.NetworkRequest.keyStatusCode, diff --git a/Sources/EmbraceCore/Capture/OneTimeServices/AppInfoCaptureService.swift b/Sources/EmbraceCore/Capture/OneTimeServices/AppInfoCaptureService.swift index 5d50592e..b9f66fb4 100644 --- a/Sources/EmbraceCore/Capture/OneTimeServices/AppInfoCaptureService.swift +++ b/Sources/EmbraceCore/Capture/OneTimeServices/AppInfoCaptureService.swift @@ -61,5 +61,20 @@ class AppInfoCaptureService: ResourceCaptureService { key: AppResourceKey.processIdentifier.rawValue, value: .string(ProcessIdentifier.current.hex) ) + + // process start time + if let processStartTime = ProcessMetadata.startTime { + addResource( + key: AppResourceKey.processStartTime.rawValue, + value: .int(processStartTime.nanosecondsSince1970Truncated) + ) + } + + // pre-warm + let isPreWarm = ProcessInfo.processInfo.environment["ActivePrewarm"] == "1" + addResource( + key: AppResourceKey.processPreWarm.rawValue, + value: .bool(isPreWarm) + ) } } diff --git a/Sources/EmbraceCore/Capture/OneTimeServices/DeviceInfoCaptureService.swift b/Sources/EmbraceCore/Capture/OneTimeServices/DeviceInfoCaptureService.swift index 15c579f7..16503470 100644 --- a/Sources/EmbraceCore/Capture/OneTimeServices/DeviceInfoCaptureService.swift +++ b/Sources/EmbraceCore/Capture/OneTimeServices/DeviceInfoCaptureService.swift @@ -69,5 +69,11 @@ class DeviceInfoCaptureService: ResourceCaptureService { key: DeviceResourceKey.model.rawValue, value: .string(EMBDevice.model) ) + + // architecture + addResource( + key: DeviceResourceKey.architecture.rawValue, + value: .string(EMBDevice.architecture) + ) } } diff --git a/Sources/EmbraceCore/Capture/System/LowMemoryWarningCaptureService.swift b/Sources/EmbraceCore/Capture/System/LowMemoryWarningCaptureService.swift index 740fc5f9..5b014e10 100644 --- a/Sources/EmbraceCore/Capture/System/LowMemoryWarningCaptureService.swift +++ b/Sources/EmbraceCore/Capture/System/LowMemoryWarningCaptureService.swift @@ -22,7 +22,7 @@ public class LowMemoryWarningCaptureService: CaptureService { } public override func onInstall() { - // hardcoded string so we dont have to use UIApplication + // hardcoded string so we don't have to use UIApplication NotificationCenter.default.addObserver( self, selector: #selector(didReceiveMemoryWarning), diff --git a/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService+Options.swift b/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService+Options.swift index 856b5ab4..43d08679 100644 --- a/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService+Options.swift +++ b/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService+Options.swift @@ -15,7 +15,7 @@ extension TapCaptureService { /// Defines wether the service should capture the coordinates of the taps. @objc public let captureTapCoordinates: Bool - /// Delegate used to decide if each indivudual tap should be recorded or not. + /// Delegate used to decide if each individual tap should be recorded or not. @objc public let delegate: TapCaptureServiceDelegate? @objc public init( diff --git a/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService.swift b/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService.swift index 2215f20a..ba1c3b79 100644 --- a/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService.swift +++ b/Sources/EmbraceCore/Capture/UX/Tap/TapCaptureService.swift @@ -46,7 +46,7 @@ public final class TapCaptureService: CaptureService { try swizzler?.install() } catch let exception { - Embrace.logger.error("An error ocurred while swizzling UIWindow.sendEvent: \(exception.localizedDescription)") + Embrace.logger.error("An error occurred while swizzling UIWindow.sendEvent: \(exception.localizedDescription)") } } diff --git a/Sources/EmbraceCore/Capture/UX/View/ViewCaptureService.swift b/Sources/EmbraceCore/Capture/UX/View/ViewCaptureService.swift index 7b6297a5..db82d607 100644 --- a/Sources/EmbraceCore/Capture/UX/View/ViewCaptureService.swift +++ b/Sources/EmbraceCore/Capture/UX/View/ViewCaptureService.swift @@ -8,7 +8,7 @@ import EmbraceCaptureService import EmbraceCommonInternal import EmbraceOTelInternal -/// Service that generates OpenTelemtry spans for `UIViewControllers`. +/// Service that generates OpenTelemetry spans for `UIViewControllers`. /// The spans start on `viewDidAppear` and end on `viewDidDisappear`. @objc(EMBViewCaptureService) public final class ViewCaptureService: CaptureService { diff --git a/Sources/EmbraceCore/Capture/WebView/WebViewCaptureService.swift b/Sources/EmbraceCore/Capture/WebView/WebViewCaptureService.swift index 8ddc2ba2..24a5d987 100644 --- a/Sources/EmbraceCore/Capture/WebView/WebViewCaptureService.swift +++ b/Sources/EmbraceCore/Capture/WebView/WebViewCaptureService.swift @@ -65,7 +65,7 @@ public final class WebViewCaptureService: CaptureService { } private func initializeSwizzlers() { - swizzlers.append(WKWebViewSetNativationDelegateSwizzler(proxy: proxy)) + swizzlers.append(WKWebViewSetNavigationDelegateSwizzler(proxy: proxy)) swizzlers.append(WKWebViewLoadRequestSwizzler()) swizzlers.append(WKWebViewLoadHTMLStringSwizzler()) swizzlers.append(WKWebViewLoadFileURLSwizzler()) @@ -111,7 +111,7 @@ public final class WebViewCaptureService: CaptureService { } } -struct WKWebViewSetNativationDelegateSwizzler: Swizzlable { +struct WKWebViewSetNavigationDelegateSwizzler: Swizzlable { typealias ImplementationType = @convention(c) (WKWebView, Selector, WKNavigationDelegate) -> Void typealias BlockImplementationType = @convention(block) (WKWebView, WKNavigationDelegate) -> Void static var selector: Selector = #selector(setter: WKWebView.navigationDelegate) @@ -151,7 +151,7 @@ struct WKWebViewLoadRequestSwizzler: Swizzlable { try swizzleInstanceMethod { originalImplementation -> BlockImplementationType in return { webView, request in if webView.navigationDelegate == nil { - webView.navigationDelegate = nil // forcefuly trigger setNagivationDelegate swizzler + webView.navigationDelegate = nil // forceful trigger setNavigationDelegate swizzler } return originalImplementation(webView, Self.selector, request) @@ -174,7 +174,7 @@ struct WKWebViewLoadHTMLStringSwizzler: Swizzlable { try swizzleInstanceMethod { originalImplementation -> BlockImplementationType in return { webView, htmlString, url in if webView.navigationDelegate == nil { - webView.navigationDelegate = nil // forcefuly trigger setNagivationDelegate swizzler + webView.navigationDelegate = nil // forcefully trigger setNavigationDelegate swizzler } return originalImplementation(webView, Self.selector, htmlString, url) @@ -197,7 +197,7 @@ struct WKWebViewLoadFileURLSwizzler: Swizzlable { try swizzleInstanceMethod { originalImplementation -> BlockImplementationType in return { webView, fileUrl, readAccessURL in if webView.navigationDelegate == nil { - webView.navigationDelegate = nil // forcefuly trigger setNagivationDelegate swizzler + webView.navigationDelegate = nil // forcefully trigger setNavigationDelegate swizzler } return originalImplementation(webView, Self.selector, fileUrl, readAccessURL) @@ -222,7 +222,7 @@ struct WKWebViewLoadDataSwizzler: Swizzlable { try swizzleInstanceMethod { originalImplementation -> BlockImplementationType in return { webView, data, mimeType, encoding, url in if webView.navigationDelegate == nil { - webView.navigationDelegate = nil // forcefuly trigger setNagivationDelegate swizzler + webView.navigationDelegate = nil // forcefully trigger setNavigationDelegate swizzler } return originalImplementation(webView, Self.selector, data, mimeType, encoding, url) diff --git a/Sources/EmbraceCore/Embrace.swift b/Sources/EmbraceCore/Embrace.swift index b5ecb0f0..3460e423 100644 --- a/Sources/EmbraceCore/Embrace.swift +++ b/Sources/EmbraceCore/Embrace.swift @@ -142,8 +142,8 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta self.deviceId = DeviceIdentifier.retrieve(from: storage) self.upload = Embrace.createUpload(options: options, deviceId: deviceId.hex) self.captureServices = try CaptureServices(options: options, storage: storage, upload: upload) - self.config = Embrace.createConfig(options: options, deviceId: deviceId.hex) - self.sessionController = SessionController(storage: storage, upload: upload) + self.config = Embrace.createConfig(options: options, deviceId: deviceId) + self.sessionController = SessionController(storage: storage, upload: upload, config: config) self.sessionLifecycle = Embrace.createSessionLifecycle(controller: sessionController) self.metadata = MetadataHandler(storage: storage, sessionController: sessionController) self.logController = logControllable ?? LogController( @@ -182,6 +182,9 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta throw EmbraceSetupError.invalidThread("Embrace must be started on the main thread") } + // must be called on main thread in order to fetch the app state + sessionLifecycle.setup() + Embrace.synchronizationQueue.sync { guard started == false else { Embrace.logger.warning("Embrace was already started!") @@ -209,13 +212,11 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta storage: self?.storage, upload: self?.upload, otel: self, + logController: self?.logController, currentSessionId: self?.sessionController.currentSession?.id, crashReporter: self?.captureServices.crashReporter ) - // upload persisted logs - self?.logController.uploadAllPersistedLogs() - // retry any remaining cached upload data self?.upload?.retryCachedData() } @@ -250,10 +251,11 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta sessionLifecycle.endSession() } - /// Called everytime the remote config changes + /// Called every time the remote config changes @objc private func onConfigUpdated() { if let config = config { - Embrace.logger.limits = InternalLogLimits(config: config) + Embrace.logger.limits = config.internalLogLimits } } } + diff --git a/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift b/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift index 346cebdc..593eec75 100644 --- a/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift +++ b/Sources/EmbraceCore/FileSystem/EmbraceFileSystem.swift @@ -16,10 +16,10 @@ public struct EmbraceFileSystem { /// Returns the path to the system directory that is the root directory for storage. /// When `appGroupId` is present, will be a URL to an app group container - /// If not present, will be a path to the user's applicaton support directory. + /// If not present, will be a path to the user's application support directory. /// /// - Note: On tvOS, if `appGroupId` is not present this will be a path to the user's Cache directory. - /// tvOS is an always connected system an long term persistented data is not permitted + /// tvOS is an always connected system an long term persisted data is not permitted private static func systemDirectory(appGroupId: String? = nil) -> URL? { // if the app group identifier is set, we use the shared container provided by the OS if let appGroupId = appGroupId { @@ -53,7 +53,8 @@ public struct EmbraceFileSystem { /// ``` /// - Parameters: /// - name: The name of the subdirectory - /// - partitionIdentifier: The main paritition identifier to use + /// - partitionIdentifier: The main partition identifier to use + /// 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 { diff --git a/Sources/EmbraceCore/Internal/Embrace+Config.swift b/Sources/EmbraceCore/Internal/Embrace+Config.swift index 3c0def0f..630f1e01 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Config.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Config.swift @@ -6,25 +6,41 @@ import Foundation import EmbraceObjCUtilsInternal import EmbraceConfigInternal import EmbraceCommonInternal +import EmbraceConfiguration extension Embrace { /// Creates `EmbraceConfig` object static func createConfig( options: Embrace.Options, - deviceId: String - ) -> EmbraceConfig? { + deviceId: DeviceIdentifier + ) -> EmbraceConfig { + return EmbraceConfig( + configurable: runtimeConfiguration(from: options, deviceId: deviceId), + options: .init(), + notificationCenter: Embrace.notificationCenter, + logger: Embrace.logger + ) + } - guard let appId = options.appId else { - return nil + private static func runtimeConfiguration( + from options: Embrace.Options, + deviceId: DeviceIdentifier + ) -> EmbraceConfigurable { + if let configImpl = options.runtimeConfiguration { + return configImpl } - guard let endpoints = options.endpoints else { - return nil + guard let configBaseURL = options.endpoints?.configBaseURL else { + return DefaultConfig() } - let configOptions = EmbraceConfig.Options( - apiBaseUrl: endpoints.configBaseURL, + guard let appId = options.appId else { + return DefaultConfig() + } + + let options = RemoteConfig.Options( + apiBaseUrl: configBaseURL, queue: DispatchQueue(label: "com.embrace.config"), appId: appId, deviceId: deviceId, @@ -34,10 +50,10 @@ extension Embrace { userAgent: EmbraceMeta.userAgent ) - return EmbraceConfig( - options: configOptions, - notificationCenter: Embrace.notificationCenter, - logger: Embrace.logger + let usedDigits = UInt(6) + return RemoteConfig( + options: options, + logger: logger ) } } diff --git a/Sources/EmbraceCore/Internal/Embrace+Setup.swift b/Sources/EmbraceCore/Internal/Embrace+Setup.swift index 2d7fc559..afa8ab73 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Setup.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Setup.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceCommonInternal +import EmbraceConfigInternal import EmbraceOTelInternal import EmbraceStorageInternal import EmbraceUploadInternal diff --git a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift index 8e0fc77a..a608277e 100644 --- a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift +++ b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift @@ -13,7 +13,7 @@ extension DeviceIdentifier { // retrieve from storage if let storage = storage { do { - if let resource = try storage.fetchRequriedPermanentResource(key: resourceKey) { + if let resource = try storage.fetchRequiredPermanentResource(key: resourceKey) { if let uuid = resource.uuidValue { return DeviceIdentifier(value: uuid) } diff --git a/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift b/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift index a6383fbc..d6a21245 100644 --- a/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift +++ b/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift @@ -7,6 +7,7 @@ import EmbraceCommonInternal import EmbraceOTelInternal import EmbraceStorageInternal import EmbraceConfigInternal +import EmbraceConfiguration class DefaultInternalLogger: InternalLogger { @@ -47,12 +48,12 @@ class DefaultInternalLogger: InternalLogger { NotificationCenter.default.removeObserver(self) } - @objc func onSessionStart(noticication: Notification) { - currentSession = noticication.object as? SessionRecord + @objc func onSessionStart(notification: Notification) { + currentSession = notification.object as? SessionRecord counter.removeAll() } - @objc func onSessionEnd(noticication: Notification) { + @objc func onSessionEnd(notification: Notification) { currentSession = nil } @@ -107,7 +108,7 @@ class DefaultInternalLogger: InternalLogger { } private func sendOTelLog(level: LogLevel, message: String, attributes: [String: String]) { - let limit = limits.forLogLevel(level) + let limit = limits.limit(for: level) guard limit > 0 else { return } @@ -145,36 +146,8 @@ class DefaultInternalLogger: InternalLogger { } } -struct InternalLogLimits { - let trace: Int - let debug: Int - let info: Int - let warning: Int - let error: Int - - init () { - self.init(trace: 0, debug: 0, info: 0, warning: 0, error: 3) - } - - init(config: EmbraceConfig) { - self.init( - trace: config.internalLogsTraceLimit, - debug: config.internalLogsDebugLimit, - info: config.internalLogsInfoLimit, - warning: config.internalLogsWarningLimit, - error: config.internalLogsErrorLimit - ) - } - - init(trace: Int, debug: Int, info: Int, warning: Int, error: Int) { - self.trace = trace - self.debug = debug - self.info = info - self.warning = warning - self.error = error - } - - func forLogLevel(_ level: LogLevel) -> Int { +extension InternalLogLimits { + func limit(for level: LogLevel) -> UInt { switch level { case .trace: return trace case .debug: return debug diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 6166e5d4..b9c70992 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -13,6 +13,7 @@ protocol LogBatcherDelegate: AnyObject { protocol LogBatcher { func addLogRecord(logRecord: LogRecord) + func renewBatch(withLogs logRecords: [LogRecord]) } class DefaultLogBatcher: LogBatcher { @@ -50,7 +51,7 @@ class DefaultLogBatcher: LogBatcher { } } -private extension DefaultLogBatcher { +internal extension DefaultLogBatcher { func renewBatch(withLogs logRecords: [LogRecord] = []) { processorQueue.async { guard let batch = self.batch else { diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift index 4287ba67..6ceb4436 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift @@ -26,6 +26,22 @@ class StorageEmbraceLogExporter: LogRecordExporter { self.state = state self.logBatcher = logBatcher self.validation = LogDataValidation(validators: validators) + + NotificationCenter.default.addObserver( + self, + selector: #selector(onSessionEnd), + name: .embraceSessionWillEnd, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func onSessionEnd(noticication: Notification) { + // forcefully start a new batch of logs when a session ends + logBatcher.renewBatch(withLogs: []) } func export(logRecords: [ReadableLogRecord], explicitTimeout: TimeInterval?) -> ExportResult { diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidator.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidator.swift index 6e59a7f2..02d0785c 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidator.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidator.swift @@ -13,7 +13,7 @@ class LengthOfBodyValidator: LogDataValidator { let allowedCharacterCount: ClosedRange - init(allowedCharacterCount: ClosedRange = 1...4000) { + init(allowedCharacterCount: ClosedRange = 0...4000) { self.allowedCharacterCount = allowedCharacterCount } diff --git a/Sources/EmbraceCore/Internal/Logs/LogController.swift b/Sources/EmbraceCore/Internal/Logs/LogController.swift index 2e72ff3e..ffb0a7e2 100644 --- a/Sources/EmbraceCore/Internal/Logs/LogController.swift +++ b/Sources/EmbraceCore/Internal/Logs/LogController.swift @@ -6,6 +6,7 @@ import Foundation import EmbraceStorageInternal import EmbraceUploadInternal import EmbraceCommonInternal +import EmbraceSemantics protocol LogControllable: LogBatcherDelegate { func uploadAllPersistedLogs() @@ -46,7 +47,7 @@ class LogController: LogControllable { extension LogController { func batchFinished(withLogs logs: [LogRecord]) { do { - guard let sessionId = sessionController?.currentSession?.id else { + guard let sessionId = sessionController?.currentSession?.id, logs.count > 0 else { return } let resourcePayload = try createResourcePayload(sessionId: sessionId) @@ -63,23 +64,40 @@ private extension LogController { guard batches.count > 0 else { return } - guard let sessionId = sessionController?.currentSession?.id else { - return - } - do { - let resourcePayload = try createResourcePayload(sessionId: sessionId) - let metadataPayload = try createMetadataPayload(sessionId: sessionId) + for batch in batches { + do { + guard batch.logs.count > 0 else { + continue + } + + // Since we always end batches when a session ends + // all the logs still in storage when the app starts should come + // from the last session before the app closes. + // + // We grab the first valid sessionId from the stored logs + // and assume all of them come from the same session. + // + // If we can't find a sessionId, we use the processId instead + + var sessionId: SessionIdentifier? + if let log = batch.logs.first(where: { $0.attributes[LogSemantics.keySessionId] != nil }) { + sessionId = SessionIdentifier(string: log.attributes[LogSemantics.keySessionId]?.description) + } + + let processId = batch.logs[0].processIdentifier + + let resourcePayload = try createResourcePayload(sessionId: sessionId, processId: processId) + let metadataPayload = try createMetadataPayload(sessionId: sessionId, processId: processId) - batches.forEach { batch in send( logs: batch.logs, resourcePayload: resourcePayload, metadataPayload: metadataPayload ) + } catch let exception { + Error.couldntCreatePayload(reason: exception.localizedDescription).log() } - } catch let exception { - Error.couldntCreatePayload(reason: exception.localizedDescription).log() } } @@ -137,24 +155,41 @@ private extension LogController { return batches } - func createResourcePayload(sessionId: SessionIdentifier) throws -> ResourcePayload { + func createResourcePayload(sessionId: SessionIdentifier?, + processId: ProcessIdentifier = ProcessIdentifier.current + ) throws -> ResourcePayload { guard let storage = storage else { throw Error.couldntAccessStorageModule } - let resources = try storage.fetchResourcesForSessionId(sessionId) + + var resources: [MetadataRecord] = [] + + if let sessionId = sessionId { + resources = try storage.fetchResourcesForSessionId(sessionId) + } else { + resources = try storage.fetchResourcesForProcessId(processId) + } + return ResourcePayload(from: resources) } - func createMetadataPayload(sessionId: SessionIdentifier) throws -> MetadataPayload { + func createMetadataPayload(sessionId: SessionIdentifier?, + processId: ProcessIdentifier = ProcessIdentifier.current + ) throws -> MetadataPayload { guard let storage = storage else { throw Error.couldntAccessStorageModule } var metadata: [MetadataRecord] = [] - let properties = try storage.fetchCustomPropertiesForSessionId(sessionId) - let tags = try storage.fetchPersonaTagsForSessionId(sessionId) - metadata.append(contentsOf: properties) - metadata.append(contentsOf: tags) + + if let sessionId = sessionId { + let properties = try storage.fetchCustomPropertiesForSessionId(sessionId) + let tags = try storage.fetchPersonaTagsForSessionId(sessionId) + metadata.append(contentsOf: properties) + metadata.append(contentsOf: tags) + } else { + metadata = try storage.fetchPersonaTagsForProcessId(processId) + } return MetadataPayload(from: metadata) } diff --git a/Sources/EmbraceCore/Internal/ProcessMetadata.swift b/Sources/EmbraceCore/Internal/ProcessMetadata.swift index d15ee9ee..59859cc2 100644 --- a/Sources/EmbraceCore/Internal/ProcessMetadata.swift +++ b/Sources/EmbraceCore/Internal/ProcessMetadata.swift @@ -10,7 +10,7 @@ enum ProcessMetadata { } extension ProcessMetadata { /// The Date at which this process started - /// Retreived via `sysctl` and `kinfo_proc.kp_proc` + /// Retrieved via `sysctl` and `kinfo_proc.kp_proc` static var startTime: Date? = { // Allocate memory let infoPointer = UnsafeMutablePointer.allocate(capacity: 1) diff --git a/Sources/EmbraceCore/Internal/ResourceKeys/AppResourceKey.swift b/Sources/EmbraceCore/Internal/ResourceKeys/AppResourceKey.swift index 9b94d3d5..5c9b6162 100644 --- a/Sources/EmbraceCore/Internal/ResourceKeys/AppResourceKey.swift +++ b/Sources/EmbraceCore/Internal/ResourceKeys/AppResourceKey.swift @@ -14,4 +14,6 @@ public enum AppResourceKey: String, Codable { case buildID = "emb.app.build_id" case sdkVersion = "emb.sdk.version" case processIdentifier = "emb.process_identifier" + case processStartTime = "emb.process_start_time" + case processPreWarm = "emb.process_pre_warm" } diff --git a/Sources/EmbraceCore/Options/Embrace+Options.swift b/Sources/EmbraceCore/Options/Embrace+Options.swift index db9aaad2..1bc01b4b 100644 --- a/Sources/EmbraceCore/Options/Embrace+Options.swift +++ b/Sources/EmbraceCore/Options/Embrace+Options.swift @@ -6,6 +6,8 @@ import Foundation import EmbraceCaptureService import EmbraceCommonInternal import EmbraceOTelInternal +import EmbraceConfigInternal +import EmbraceConfiguration extension Embrace { @@ -20,6 +22,7 @@ extension Embrace { @objc public let crashReporter: CrashReporter? @objc public let logLevel: LogLevel @objc public let export: OpenTelemetryExport? + @objc public let runtimeConfiguration: EmbraceConfigurable? /// Default initializer for `Embrace.Options` that requires an array of `CaptureServices` to be passed. /// @@ -53,6 +56,7 @@ extension Embrace { self.crashReporter = crashReporter self.logLevel = logLevel self.export = export + self.runtimeConfiguration = nil } /// Initializer for `Embrace.Options` that does not require an appId. @@ -65,12 +69,14 @@ extension Embrace { /// - captureServices: The `CaptureServices` to be installed. /// - crashReporter: The `CrashReporter` to be installed. /// - logLevel: The `LogLevel` to use for console logs. + /// - runtimeConfiguration: An object to control runtime behavior of the SDK itself. @objc public init( export: OpenTelemetryExport, platform: Platform = .default, captureServices: [CaptureService], crashReporter: CrashReporter?, - logLevel: LogLevel = .default + logLevel: LogLevel = .default, + runtimeConfiguration: EmbraceConfigurable = .default ) { self.appId = nil self.appGroupId = nil @@ -80,12 +86,13 @@ extension Embrace { self.crashReporter = crashReporter self.logLevel = logLevel self.export = export + self.runtimeConfiguration = runtimeConfiguration } } } internal extension Embrace.Options { - /// Valiate Options object to make sure it has not been configured ambiguously + /// Validate Options object to make sure it has not been configured ambiguously func validate() throws { try validateAppId() try validateGroupId() diff --git a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift index 19fc0228..95f1a47e 100644 --- a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift @@ -15,7 +15,7 @@ class SessionPayloadBuilder { do { // fetch resource - resource = try storage.fetchRequriedPermanentResource(key: resourceName) + resource = try storage.fetchRequiredPermanentResource(key: resourceName) } catch { Embrace.logger.debug("Error fetching \(resourceName) resource!") } diff --git a/Sources/EmbraceCore/Payload/DeviceInfoPayload.swift b/Sources/EmbraceCore/Payload/DeviceInfoPayload.swift deleted file mode 100644 index a8f42cd8..00000000 --- a/Sources/EmbraceCore/Payload/DeviceInfoPayload.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceCommonInternal -import EmbraceStorageInternal - -struct DeviceInfoPayload: Codable { - var isJailbroken: Bool? - var locale: String? - var timeZone: String? - var totalDiskSpace: Int? - var osVersion: String? - var osBuild: String? - let osType: String = "iOS" // Hardcode to iOS for /v1/sessions endpoint - var osVariant: String? - var architecture: String? - var model: String? - var manufacturer: String? = "Apple" - var screenResolution: String? - - enum CodingKeys: String, CodingKey { - case isJailbroken = "jb" - case locale = "lc" - case timeZone = "tz" - case totalDiskSpace = "ms" - case osVersion = "ov" - case osBuild = "ob" - case osType = "os" - case osVariant = "oa" - case architecture = "da" - case model = "do" - case manufacturer = "dm" - case screenResolution = "sr" - } - - init(with resources: [MetadataRecord]) { - resources.forEach { resource in - guard let key: DeviceResourceKey = DeviceResourceKey(rawValue: resource.key) else { - return - } - - switch key { - case .isJailbroken: - self.isJailbroken = resource.boolValue - case .locale: - self.locale = resource.stringValue - case .timezone: - self.timeZone = resource.stringValue - case .totalDiskSpace: - self.totalDiskSpace = resource.integerValue - case .osVersion: - self.osVersion = resource.stringValue - case .osBuild: - self.osBuild = resource.stringValue - case .architecture: - self.architecture = resource.stringValue - case .model: - self.model = resource.stringValue - case .manufacturer: - self.manufacturer = resource.stringValue - case .screenResolution: - self.screenResolution = resource.stringValue - case .osVariant: - self.osVariant = resource.stringValue - case .osName, .osDescription, .osType: - break - } - } - } -} diff --git a/Sources/EmbraceCore/Payload/PayloadEnvelope.swift b/Sources/EmbraceCore/Payload/PayloadEnvelope.swift index d8f493b0..605ad03c 100644 --- a/Sources/EmbraceCore/Payload/PayloadEnvelope.swift +++ b/Sources/EmbraceCore/Payload/PayloadEnvelope.swift @@ -7,7 +7,7 @@ import Foundation struct PayloadEnvelope: Encodable { var resource: ResourcePayload var metadata: MetadataPayload - var version: String = "1.0" // TODO: Make this the actual version + var version: String = "1.0" var type: String var data = [String: T]() } diff --git a/Sources/EmbraceCore/Payload/ResourcePayload.swift b/Sources/EmbraceCore/Payload/ResourcePayload.swift index 98791651..1edd23f9 100644 --- a/Sources/EmbraceCore/Payload/ResourcePayload.swift +++ b/Sources/EmbraceCore/Payload/ResourcePayload.swift @@ -29,6 +29,8 @@ struct ResourcePayload: Codable { var appVersion: String? var appBundleId: String? var processIdentifier: String? + var processStartTime: Int? + var processPreWarm: Bool? var additionalResources: [String: String] = [:] private let excludedKeys: Set = [ @@ -61,6 +63,8 @@ struct ResourcePayload: Codable { case appVersion = "app_version" case appBundleId = "app_bundle_id" case processIdentifier = "process_identifier" + case processStartTime = "process_start_time" + case processPreWarm = "process_pre_warm" } func encode(to encoder: Encoder) throws { @@ -87,6 +91,8 @@ struct ResourcePayload: Codable { try container.encode(appVersion, forKey: .appVersion) try container.encode(appBundleId, forKey: .appBundleId) try container.encode(processIdentifier, forKey: .processIdentifier) + try container.encode(processStartTime, forKey: .processStartTime) + try container.encode(processPreWarm, forKey: .processPreWarm) var additionalResourcesContainer = encoder.container(keyedBy: StringDictionaryCodingKeys.self) for (key, value) in additionalResources { @@ -126,7 +132,12 @@ struct ResourcePayload: Codable { self.processIdentifier = resource.stringValue case .buildID: self.buildId = resource.stringValue + case .processStartTime: + self.processStartTime = resource.integerValue + case .processPreWarm: + self.processPreWarm = resource.boolValue } + } else if let key = DeviceResourceKey(rawValue: resource.key) { switch key { case .isJailbroken: diff --git a/Sources/EmbraceCore/Payload/UserInfoPayload.swift b/Sources/EmbraceCore/Payload/UserInfoPayload.swift deleted file mode 100644 index ed2cba5b..00000000 --- a/Sources/EmbraceCore/Payload/UserInfoPayload.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal - -struct UserInfoPayload: Codable { - - var username: String? - var identifier: String? - var email: String? - - enum CodingKeys: String, CodingKey { - case username = "un" - case identifier = "id" - case email = "em" - } - - init(with properties: [MetadataRecord]) { - properties.forEach { property in - guard let key: UserResourceKey = UserResourceKey(rawValue: property.key) else { - return - } - let value = property.stringValue - - switch key { - case .name: - username = value - case .identifier: - identifier = value - case .email: - email = value - } - } - } -} diff --git a/Sources/EmbraceCore/Public/Embrace+CrashReporter.swift b/Sources/EmbraceCore/Public/Embrace+CrashReporter.swift index ae2cdf3c..cb8fc4e0 100644 --- a/Sources/EmbraceCore/Public/Embrace+CrashReporter.swift +++ b/Sources/EmbraceCore/Public/Embrace+CrashReporter.swift @@ -51,11 +51,11 @@ extension Embrace { /// - key: The key for the attribute. /// - value: The value associated with the given key. public func appendCrashInfo(key: String, value: String) throws { - guard let crashRporter = captureServices.crashReporter else { + guard let crashReporter = captureServices.crashReporter else { throw EmbraceCrashReportError.noCrashReporterAvailable } - guard let extendableCrashReporter = crashRporter as? ExtendableCrashReporter else { + guard let extendableCrashReporter = crashReporter as? ExtendableCrashReporter else { throw EmbraceCrashReportError.noCrashReporterAvailable } diff --git a/Sources/EmbraceCore/Public/Embrace+OTel.swift b/Sources/EmbraceCore/Public/Embrace+OTel.swift index e30d7921..1a4cea5a 100644 --- a/Sources/EmbraceCore/Public/Embrace+OTel.swift +++ b/Sources/EmbraceCore/Public/Embrace+OTel.swift @@ -149,11 +149,11 @@ extension Embrace: EmbraceOpenTelemetry { ) /* - If we want to keep this method cleaner, we could move this logcto `EmbraceLogAttributesBuilder` + If we want to keep this method cleaner, we could move this log to `EmbraceLogAttributesBuilder` However that would cause to always add a frame to the stacktrace. */ if stackTraceBehavior == .default && (severity == .warn || severity == .error) { - var stackTrace: [String] = Thread.callStackSymbols + let stackTrace: [String] = Thread.callStackSymbols attributesBuilder.addStackTrace(stackTrace) } diff --git a/Sources/EmbraceCore/Public/Events/Breadcrumb.swift b/Sources/EmbraceCore/Public/Events/Breadcrumb.swift index 799b9328..116081f1 100644 --- a/Sources/EmbraceCore/Public/Events/Breadcrumb.swift +++ b/Sources/EmbraceCore/Public/Events/Breadcrumb.swift @@ -8,7 +8,7 @@ import EmbraceCommonInternal import EmbraceSemantics import OpenTelemetryApi -/// Class used to represent a Breadcrum as a SpanEvent. +/// Class used to represent a Breadcrumb as a SpanEvent. /// Usage example: /// `Embrace.client?.add(.breadcrumb("This is a breadcrumb"))` @objc(EMBBreadcrumb) @@ -22,10 +22,10 @@ public class Breadcrumb: NSObject, SpanEvent { timestamp: Date = Date(), attributes: [String: AttributeValue] ) { - self.name = SpanEventSemantics.Bradcrumb.name + self.name = SpanEventSemantics.Breadcrumb.name self.timestamp = timestamp self.attributes = attributes - self.attributes[SpanEventSemantics.Bradcrumb.keyMessage] = .string(message) + self.attributes[SpanEventSemantics.Breadcrumb.keyMessage] = .string(message) self.attributes[SpanEventSemantics.keyEmbraceType] = .string(SpanType.breadcrumb.rawValue) } } diff --git a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift index 09f52f78..cf046114 100644 --- a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift +++ b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift @@ -13,6 +13,7 @@ class UnsentDataHandler { storage: EmbraceStorage?, upload: EmbraceUpload?, otel: EmbraceOpenTelemetry?, + logController: LogControllable? = nil, currentSessionId: SessionIdentifier? = nil, crashReporter: CrashReporter? = nil ) { @@ -22,6 +23,9 @@ class UnsentDataHandler { return } + // send any logs in storage first before we clean up the resources + logController?.uploadAllPersistedLogs() + // if we have a crash reporter, we fetch the unsent crash reports first // and save their identifiers to the corresponding sessions if let crashReporter = crashReporter { @@ -261,7 +265,7 @@ class UnsentDataHandler { // since spans are only sent when included in a session // all of these would never be sent anymore, so they can be safely removed // if no session is found, all closed spans can be safely removed as well - let oldestSession = try storage.fetchOldestSesssion() + let oldestSession = try storage.fetchOldestSession() try storage.cleanUpSpans(date: oldestSession?.startTime) } catch { @@ -275,7 +279,7 @@ class UnsentDataHandler { // we use the latest session on storage to determine the `endTime` // since we need to have a valid `endTime` for these spans, we default // to `Date()` if we don't have a session - let latestSession = try storage.fetchLatestSesssion(ignoringCurrentSessionId: currentSessionId) + let latestSession = try storage.fetchLatestSession(ignoringCurrentSessionId: currentSessionId) let endTime = (latestSession?.endTime ?? latestSession?.lastHeartbeatTime) ?? Date() try storage.closeOpenSpans(endTime: endTime) } catch { diff --git a/Sources/EmbraceCore/Session/SessionControllable.swift b/Sources/EmbraceCore/Session/SessionControllable.swift index feb47e61..4e7b9454 100644 --- a/Sources/EmbraceCore/Session/SessionControllable.swift +++ b/Sources/EmbraceCore/Session/SessionControllable.swift @@ -13,7 +13,7 @@ protocol SessionControllable: AnyObject { var currentSession: SessionRecord? { get } @discardableResult - func startSession(state: SessionState) -> SessionRecord + func startSession(state: SessionState) -> SessionRecord? @discardableResult func endSession() -> Date diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index b61647e2..f6b3d2ea 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceCommonInternal +import EmbraceConfigInternal import EmbraceStorageInternal import EmbraceUploadInternal import EmbraceOTelInternal @@ -35,18 +36,27 @@ class SessionController: SessionControllable { weak var storage: EmbraceStorage? weak var upload: EmbraceUpload? + weak var config: EmbraceConfig? + + private var backgroundSessionsEnabled: Bool { + return config?.isBackgroundSessionEnabled == true + } + let heartbeat: SessionHeartbeat let queue: DispatchQueue + var firstSession = true internal var notificationCenter = NotificationCenter.default init( storage: EmbraceStorage, upload: EmbraceUpload?, + config: EmbraceConfig?, heartbeatInterval: TimeInterval = SessionHeartbeat.defaultInterval ) { self.storage = storage self.upload = upload + self.config = config let heartbeatQueue = DispatchQueue(label: "com.embrace.session_heartbeat") self.heartbeat = SessionHeartbeat(queue: heartbeatQueue, interval: heartbeatInterval) @@ -65,24 +75,40 @@ class SessionController: SessionControllable { } @discardableResult - func startSession(state: SessionState) -> SessionRecord { + func startSession(state: SessionState) -> SessionRecord? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord { + func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { // end current session first if currentSession != nil { endSession() } + // detect cold start + let isColdStart = firstSession + + // Don't start background session if the config is disabled. + // + // Note: There's an exception for the cold start session: + // We start the session anyways and we drop it when it ends if + // it's still considered a background session. + // Due to how iOS works we can't know for sure the state when the + // app starts, so we need to delay the logic! + // + // + + if isColdStart == false && + state == .background && + backgroundSessionsEnabled == false { + return nil + } + // - + // we lock after end session to avoid a deadlock return lock.locked { - // detect cold start - let isColdStart = withinColdStartInterval(startTime: startTime) - // create session span let newId = SessionIdentifier.random let span = SessionSpanUtils.span(id: newId, startTime: startTime, state: state, coldStart: isColdStart) @@ -109,6 +135,8 @@ class SessionController: SessionControllable { // post notification notificationCenter.post(name: .embraceSessionDidStart, object: session) + firstSession = false + return session } } @@ -121,11 +149,22 @@ class SessionController: SessionControllable { return lock.locked { // stop heartbeat heartbeat.stop() + let now = Date() + + // If the session is a background session and background sessions + // are disabled in the config, we drop the session! + // + + if currentSession?.coldStart == true && + currentSession?.state == SessionState.background.rawValue && + backgroundSessionsEnabled == false { + delete() + return now + } + // - // post notification notificationCenter.post(name: .embraceSessionWillEnd, object: currentSession) - let now = Date() currentSessionSpan?.end(time: now) SessionSpanUtils.setCleanExit(span: currentSessionSpan, cleanExit: true) @@ -171,27 +210,32 @@ class SessionController: SessionControllable { } extension SessionController { - static let allowedColdStartInterval: TimeInterval = 5.0 - - /// - Returns: `true` if ``ProcessMetadata.uptime`` is less than or equal to the allowed cold start interval. See ``iOSAppListener.minimumColdStartInterval`` - private func withinColdStartInterval(startTime: Date) -> Bool { - guard let uptime = ProcessMetadata.uptime(since: startTime), uptime >= 0 else { - return false + private func save() { + guard let storage = storage, + let session = currentSession else { + return } - return uptime <= Self.allowedColdStartInterval + do { + try storage.upsertSession(session) + } catch { + Embrace.logger.warning("Error trying to update session:\n\(error.localizedDescription)") + } } - private func save() { + private func delete() { guard let storage = storage, let session = currentSession else { return } do { - try storage.upsertSession(session) + try storage.delete(record: session) } catch { - Embrace.logger.warning("Error trying to update session:\n\(error.localizedDescription)") + Embrace.logger.warning("Error trying to delete session:\n\(error.localizedDescription)") } + + currentSession = nil + currentSessionSpan = nil } } diff --git a/Sources/EmbraceOTelInternal/Shared/EmbraceResource.swift b/Sources/EmbraceOTelInternal/Shared/EmbraceResource.swift index df51ab2b..4d219fe1 100644 --- a/Sources/EmbraceOTelInternal/Shared/EmbraceResource.swift +++ b/Sources/EmbraceOTelInternal/Shared/EmbraceResource.swift @@ -11,7 +11,7 @@ import OpenTelemetrySdk public typealias ResourceValue = AttributeValue // This representation of the `Resource` concept was necessary because -// some entities (like `LogReadeableRecord`) needs it. +// some entities (like `LogReadableRecord`) needs it. public protocol EmbraceResource { var key: String { get } var value: ResourceValue { get } diff --git a/Sources/EmbraceSemantics/SpanEvent/SpanEventSemantics+Breadcrumb.swift b/Sources/EmbraceSemantics/SpanEvent/SpanEventSemantics+Breadcrumb.swift index ae92adb3..4596ed8e 100644 --- a/Sources/EmbraceSemantics/SpanEvent/SpanEventSemantics+Breadcrumb.swift +++ b/Sources/EmbraceSemantics/SpanEvent/SpanEventSemantics+Breadcrumb.swift @@ -9,8 +9,14 @@ public extension SpanType { } public extension SpanEventSemantics { - struct Bradcrumb { - public static let name = "emb-breadcrumb" - public static let keyMessage = "message" - } + @available(*, deprecated, message: "Use Breadcrumb as this struct will be removed in future versions", renamed: "Breadcrumb") + struct Bradcrumb { + public static let name: String = Breadcrumb.name + public static let keyMessage: String = Breadcrumb.keyMessage + } + + struct Breadcrumb { + public static let name = "emb-breadcrumb" + public static let keyMessage = "message" + } } diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift index feeca738..31e53385 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift @@ -13,6 +13,7 @@ public extension EmbraceStorage { /// Class used to configure a EmbraceStorage instance class Options { + /// Determines where the db is going to be let storageMechanism: StorageMechanism /// Dictionary containing the storage limits per span type diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage.swift b/Sources/EmbraceStorageInternal/EmbraceStorage.swift index b4ee3bf2..7883b67a 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -68,7 +68,7 @@ extension EmbraceStorage { /// Deletes a record from the storage synchronously. /// - Parameter record: `PersistableRecord` to delete - /// - Returns: Boolean indicating if the record was successfuly deleted + /// - Returns: Boolean indicating if the record was successfully deleted @discardableResult public func delete(record: PersistableRecord) throws -> Bool { try dbQueue.write { db in return try record.delete(db) @@ -110,7 +110,7 @@ extension EmbraceStorage { /// - Parameters: /// - record: `PersistableRecord` to delete /// - completion: Completion block called with an `Error` on failure - /// - Returns: Boolean indicating if the record was successfuly deleted + /// - Returns: Boolean indicating if the record was successfully deleted public func deleteAsync(record: PersistableRecord, completion: ((Result) -> Void)?) { dbWriteAsync(block: { db in try record.delete(db) @@ -155,7 +155,7 @@ extension EmbraceStorage { try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) return try EmbraceStorage.getDBQueueIfPossible(at: fileURL, logger: logger) } else { - fatalError("Unsupported storage mechansim added") + fatalError("Unsupported storage mechanism added") } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 1506fc8b..270f6d4f 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -228,7 +228,7 @@ extension EmbraceStorage { } /// Returns the permanent required resource for the given key. - public func fetchRequriedPermanentResource(key: String) throws -> MetadataRecord? { + public func fetchRequiredPermanentResource(key: String) throws -> MetadataRecord? { return try fetchMetadata(key: key, type: .requiredResource, lifespan: .permanent) } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift index c0168aa0..2a3913f4 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -68,7 +68,7 @@ extension EmbraceStorage { /// Synchronously fetches the newest session in the storage, ignoring the current session if it exists. /// - Returns: The newest stored `SessionRecord`, if any - public func fetchLatestSesssion( + public func fetchLatestSession( ignoringCurrentSessionId sessionId: SessionIdentifier? = nil ) throws -> SessionRecord? { var session: SessionRecord? @@ -88,7 +88,7 @@ extension EmbraceStorage { /// Synchronously fetches the oldest session in the storage, if any. /// - Returns: The oldest stored `SessionRecord`, if any - public func fetchOldestSesssion() throws -> SessionRecord? { + public func fetchOldestSession() throws -> SessionRecord? { var session: SessionRecord? try dbQueue.read { db in session = try SessionRecord diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index f5218f0f..5f971b88 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -83,7 +83,7 @@ extension EmbraceStorage { return span } - /// Synchonously removes all the closed spans older than the given date. + /// Synchronously removes all the closed spans older than the given date. /// If no date is provided, all closed spans will be removed. /// - Parameter date: Date used to determine which spans to remove public func cleanUpSpans(date: Date? = nil) throws { @@ -100,7 +100,7 @@ extension EmbraceStorage { /// Synchronously closes all open spans with the given `endTime`. /// - Parameters: - /// - endtime: Identifier of the trace containing this span + /// - endTime: Identifier of the trace containing this span public func closeOpenSpans(endTime: Date) throws { _ = try dbQueue.write { db in try SpanRecord diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index c28093db..00ba0436 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceOTelInternal +import EmbraceCommonInternal import GRDB /// Class that handles all the cached upload data generated by the Embrace SDK. @@ -11,15 +12,14 @@ class EmbraceUploadCache { private(set) var options: EmbraceUpload.CacheOptions private(set) var dbQueue: DatabaseQueue + let logger: InternalLogger - init(options: EmbraceUpload.CacheOptions) throws { + init(options: EmbraceUpload.CacheOptions, logger: InternalLogger) throws { self.options = options - - // create base directory if necessary - try FileManager.default.createDirectory(at: options.cacheBaseUrl, withIntermediateDirectories: true) + self.logger = logger // create sqlite file - dbQueue = try DatabaseQueue(path: options.cacheFilePath) + dbQueue = try Self.createDBQueue(options: options, logger: logger) // define tables try dbQueue.write { db in @@ -125,9 +125,9 @@ class EmbraceUploadCache { /// Deletes the cached data for the given identifier. /// - Parameters: - /// - id: Identifiar of the data + /// - id: Identifier of the data /// - type: Type of the data - /// - Returns: Boolean indicating if the data was successfuly deleted + /// - Returns: Boolean indicating if the data was successfully deleted @discardableResult func deleteUploadData(id: String, type: EmbraceUploadType) throws -> Bool { guard let uploadData = try fetchUploadData(id: id, type: type) else { return false @@ -138,7 +138,7 @@ class EmbraceUploadCache { /// Deletes the cached `UploadDataRecord`. /// - Parameter uploadData: `UploadDataRecord` to delete - /// - Returns: Boolean indicating if the data was successfuly deleted + /// - Returns: Boolean indicating if the data was successfully deleted func deleteUploadData(_ uploadData: UploadDataRecord) throws -> Bool { try dbQueue.write { db in return try uploadData.delete(db) @@ -211,3 +211,67 @@ class EmbraceUploadCache { } } } + +extension EmbraceUploadCache { + + private static func createDBQueue( + options: EmbraceUpload.CacheOptions, + logger: InternalLogger + ) throws -> DatabaseQueue { + if case let .inMemory(name) = options.storageMechanism { + return try DatabaseQueue(named: name) + } else if case let .onDisk(baseURL, _) = options.storageMechanism, let fileURL = options.fileURL { + // create base directory if necessary + try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) + return try EmbraceUploadCache.getDBQueueIfPossible(at: fileURL, logger: logger) + } else { + fatalError("Unsupported storage mechansim added") + } + } + + /// Will attempt to create or open the DB File. If first attempt fails due to GRDB error, it'll assume the existing DB is corruped and try again after deleting the existing DB file. + private static func getDBQueueIfPossible(at fileURL: URL, logger: InternalLogger) throws -> DatabaseQueue { + do { + return try DatabaseQueue(path: fileURL.path) + } catch { + if let dbError = error as? DatabaseError { + logger.error( + """ + GRDB Failed to initialize EmbraceUploadCache. + Will attempt to remove existing file and create a new DB. + Message: \(dbError.message ?? "[empty message]"), + Result Code: \(dbError.resultCode), + SQLite Extended Code: \(dbError.extendedResultCode) + """ + ) + } else { + logger.error( + """ + Unknown error while trying to initialize EmbraceUploadCache: \(error) + Will attempt to recover by deleting existing DB. + """ + ) + } + } + + try EmbraceUploadCache.deleteDBFile(at: fileURL, logger: logger) + + return try DatabaseQueue(path: fileURL.path) + } + + /// Will attempt to delete the provided file. + private static func deleteDBFile(at fileURL: URL, logger: InternalLogger) throws { + do { + let fileURL = URL(fileURLWithPath: fileURL.path) + try FileManager.default.removeItem(at: fileURL) + } catch let error { + logger.error( + """ + EmbraceUploadCache failed to remove DB file. + Error: \(error.localizedDescription) + Filepath: \(fileURL) + """ + ) + } + } +} diff --git a/Sources/EmbraceUploadInternal/EmbraceUpload.swift b/Sources/EmbraceUploadInternal/EmbraceUpload.swift index b20b0f96..846fcddb 100644 --- a/Sources/EmbraceUploadInternal/EmbraceUpload.swift +++ b/Sources/EmbraceUploadInternal/EmbraceUpload.swift @@ -9,7 +9,7 @@ public protocol EmbraceLogUploader: AnyObject { func uploadLog(id: String, data: Data, completion: ((Result<(), Error>) -> Void)?) } -/// Class in charge of uploading all the data colected by the Embrace SDK. +/// Class in charge of uploading all the data collected by the Embrace SDK. public class EmbraceUpload: EmbraceLogUploader { public private(set) var options: Options @@ -32,7 +32,7 @@ public class EmbraceUpload: EmbraceLogUploader { self.logger = logger self.queue = queue - cache = try EmbraceUploadCache(options: options.cache) + cache = try EmbraceUploadCache(options: options.cache, logger: logger) urlSession = URLSession(configuration: options.urlSessionConfiguration) @@ -81,7 +81,7 @@ public class EmbraceUpload: EmbraceLogUploader { /// - Parameters: /// - id: Identifier of the session /// - data: Data of the session's payload - /// - completion: Completion block called when the data is succesfully cached, or when an `Error` occurs + /// - completion: Completion block called when the data is successfully cached, or when an `Error` occurs public func uploadSpans(id: String, data: Data, completion: ((Result<(), Error>) -> Void)?) { queue.async { [weak self] in self?.uploadData(id: id, data: data, type: .spans, completion: completion) @@ -92,7 +92,7 @@ public class EmbraceUpload: EmbraceLogUploader { /// - Parameters: /// - id: Identifier of the log batch (has no utility aside of caching) /// - data: Data of the log's payload - /// - completion: Completion block called when the data is succesfully cached, or when an `Error` occurs + /// - completion: Completion block called when the data is successfully cached, or when an `Error` occurs public func uploadLog(id: String, data: Data, completion: ((Result<(), Error>) -> Void)?) { queue.async { [weak self] in self?.uploadData(id: id, data: data, type: .log, completion: completion) diff --git a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift index fe9bdade..72747d68 100644 --- a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift +++ b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift @@ -5,17 +5,14 @@ import Foundation public extension EmbraceUpload { - class CacheOptions { - /// URL pointing to the folder where the upload cache storage will be saved - public var cacheBaseUrl: URL - - /// Name for the cache storage file - public var cacheFileName: String + enum StorageMechanism { + case inMemory(name: String) + case onDisk(baseURL: URL, fileName: String) + } - /// Full path to the storage file - public var cacheFilePath: String { - return cacheBaseUrl.appendingPathComponent(cacheFileName).path - } + class CacheOptions { + /// Determines where the db is going to be + let storageMechanism: StorageMechanism /// Determines the maximum amount of cached requests that will be cached. Use 0 to disable. public var cacheLimit: UInt @@ -37,11 +34,56 @@ public extension EmbraceUpload { return nil } - self.cacheBaseUrl = cacheBaseUrl - self.cacheFileName = cacheFileName + self.storageMechanism = .onDisk(baseURL: cacheBaseUrl, fileName: cacheFileName) + self.cacheLimit = cacheLimit + self.cacheDaysLimit = cacheDaysLimit + self.cacheSizeLimit = cacheSizeLimit + } + + public init( + named: String, + cacheLimit: UInt = 0, + cacheDaysLimit: UInt = 0, + cacheSizeLimit: UInt = 0 + ) { + self.storageMechanism = .inMemory(name: named) self.cacheLimit = cacheLimit self.cacheDaysLimit = cacheDaysLimit self.cacheSizeLimit = cacheSizeLimit } } } + +extension EmbraceUpload.CacheOptions { + /// The name of the storage item when using an inMemory storage + public var name: String? { + if case let .inMemory(name) = storageMechanism { + return name + } + return nil + } + + /// URL pointing to the folder where the storage will be saved + public var baseUrl: URL? { + if case let .onDisk(baseURL, _) = storageMechanism { + return baseURL + } + return nil + } + + /// URL pointing to the folder where the storage will be saved + public var fileName: String? { + if case let .onDisk(_, name) = storageMechanism { + return name + } + return nil + } + + /// URL to the storage file + public var fileURL: URL? { + if case let .onDisk(url, filename) = storageMechanism { + return url.appendingPathComponent(filename) + } + return nil + } +} diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigTests.swift index 7a3aa44b..8a15f711 100644 --- a/Tests/EmbraceConfigInternalTests/EmbraceConfigTests.swift +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigTests.swift @@ -1,359 +1,175 @@ // -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. // import XCTest import TestSupport @testable import EmbraceConfigInternal - -class EmbraceConfigTests: XCTestCase { - static var urlSessionConfig: URLSessionConfiguration! - - private var apiBaseUrl: String { - "https://embrace.\(testName).com/config" - } - - override func setUpWithError() throws { - let config = URLSessionConfiguration.ephemeral - config.httpMaximumConnectionsPerHost = .max - EmbraceConfigTests.urlSessionConfig = config - EmbraceConfigTests.urlSessionConfig.protocolClasses = [EmbraceHTTPMock.self] - } - - func testOptions( - testName: String = #function, - deviceId: String, - minimumUpdateInterval: TimeInterval = 0 - ) -> EmbraceConfig.Options { - return EmbraceConfig.Options( - apiBaseUrl: apiBaseUrl, - queue: DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent), - appId: TestConstants.appId, - deviceId: deviceId, - osVersion: TestConstants.osVersion, - sdkVersion: TestConstants.sdkVersion, - appVersion: TestConstants.appVersion, - userAgent: TestConstants.userAgent, - minimumUpdateInterval: minimumUpdateInterval, - urlSessionConfiguration: EmbraceConfigTests.urlSessionConfig +import EmbraceConfiguration + +final class EmbraceConfigTests: XCTestCase { + func buildConfig( + configurable: EmbraceConfigurable, + options: EmbraceConfig.Options = .init(minimumUpdateInterval: 5) + ) -> EmbraceConfig { + return EmbraceConfig( + configurable: configurable, + options: options, + notificationCenter: .default, + logger: MockLogger() ) } - func mockSuccessfulResponse() throws { - var url = try XCTUnwrap(URL(string: "\(apiBaseUrl)/v2/config")) - - if #available(iOS 16.0, *) { - url.append(queryItems: [ - .init(name: "appId", value: TestConstants.appId), - .init(name: "osVersion", value: TestConstants.osVersion), - .init(name: "appVersion", value: TestConstants.appVersion), - .init(name: "deviceId", value: TestConstants.deviceId), - .init(name: "sdkVersion", value: TestConstants.sdkVersion) - ]) - } else { - XCTFail("This will fail on versions prior to iOS 16.0") - } - - let path = Bundle.module.path( - forResource: "remote_config", - ofType: "json", - inDirectory: "Mocks" - )! - let data = try Data(contentsOf: URL(fileURLWithPath: path)) - EmbraceHTTPMock.mock(url: url, response: .withData(data, statusCode: 200)) - } + // MARK: Update if Needed - let logger = MockLogger() + func test_updateIfNeeded_returnsTrueIfUpdateOccurs() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateExpectation.expectedFulfillmentCount = 1 - func test_frequentUpdatesIgnored() throws { - // given a config with 1 hour minimum update interval - let options = testOptions( - deviceId: TestConstants.deviceId, - minimumUpdateInterval: 60 * 60 - ) + let config = buildConfig(configurable: mockConfig) - // Given the response is successful (necessary to save the `lastUpdateTime` value) - try mockSuccessfulResponse() + let result1 = config.updateIfNeeded() + XCTAssertTrue(result1) + wait(for: [mockConfig.updateExpectation]) - // Given an EmbraceConfig (executes fetch on init) - let config = EmbraceConfig(options: options, notificationCenter: NotificationCenter.default, logger: logger) + let result2 = config.updateIfNeeded() + XCTAssertFalse(result2) + } - // Wait until the fetch from init has finished - wait(timeout: .longTimeout) { - config.updating == false - } + func test_updateIfNeeded_returnsTrueIfTimeIntervalPassed() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateExpectation.expectedFulfillmentCount = 2 - // when trying to update too soon - config.updateIfNeeded() + let config = buildConfig(configurable: mockConfig, options: .init(minimumUpdateInterval: 0)) - // then the update call is ignored - let url = try XCTUnwrap(config.fetcher.buildURL()) - wait(timeout: .longTimeout) { - return EmbraceHTTPMock.requestsForUrl(url).count == 1 && - EmbraceHTTPMock.totalRequestCount() == 1 - } - } + let result1 = config.updateIfNeeded() + XCTAssertTrue(result1) - func test_frequentUpdatesNotIgnored() throws { - // given a config with 1 second minimum update interval - let options = testOptions( - deviceId: TestConstants.deviceId, - minimumUpdateInterval: 1 - ) + let result2 = config.updateIfNeeded() + XCTAssertTrue(result2) - // Given the response is successful (necessary to save the `lastUpdateTime` value) - try mockSuccessfulResponse() + wait(for: [mockConfig.updateExpectation], timeout: 1) + } - // Given an EmbraceConfig (executes fetch on init) - let config = EmbraceConfig(options: options, notificationCenter: NotificationCenter.default, logger: logger) + func test_updateIfNeeded_postsNotificationIf_configDidChange() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateCompletionParamDidUpdate = true + mockConfig.updateExpectation.expectedFulfillmentCount = 1 - // Wait until the fetch from init has finished - wait(timeout: .longTimeout) { - config.updating == false - } + let config = buildConfig(configurable: mockConfig) - // When invoking updateIfNeeded after waiting the "minimumUpdateInterval" amount assigned above - wait(delay: 1) - config.updateIfNeeded() + let notificationExpectation = expectation(forNotification: .embraceConfigUpdated, object: config) - // then the update call is not ignored - let url = try XCTUnwrap(config.fetcher.buildURL()) - wait(timeout: .longTimeout) { - return EmbraceHTTPMock.requestsForUrl(url).count == 2 && - EmbraceHTTPMock.totalRequestCount() == 2 - } + let result1 = config.updateIfNeeded() + XCTAssertTrue(result1) + wait(for: [notificationExpectation]) } - func test_forcedUpdateNotIgnored() throws { - let options = testOptions( - deviceId: TestConstants.deviceId, - minimumUpdateInterval: 60 * 60 - ) - - // Given the response is successful (necessary to save the `lastUpdateTime` value) - try mockSuccessfulResponse() + func test_updateIfNeeded_doesNot_postNotificationIf_configDidNotChange() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateCompletionParamDidUpdate = false + mockConfig.updateExpectation.expectedFulfillmentCount = 1 - // Given an EmbraceConfig (executes fetch on init) - let config = EmbraceConfig(options: options, notificationCenter: NotificationCenter.default, logger: logger) + let config = buildConfig(configurable: mockConfig) - // Wait until the fetch from init has finished - wait(timeout: .longTimeout) { - config.updating == false - } - - // When forcing an update - config.update() + let notificationExpectation = expectation(forNotification: .embraceConfigUpdated, object: config) + notificationExpectation.isInverted = true - // then the update call is not ignored - wait(timeout: .longTimeout) { - config.updating == false - } - let url = try XCTUnwrap(config.fetcher.buildURL()) - wait(timeout: .longTimeout) { - return EmbraceHTTPMock.requestsForUrl(url).count == 2 && - EmbraceHTTPMock.totalRequestCount() == 2 - } + let result1 = config.updateIfNeeded() + XCTAssertTrue(result1) + wait(for: [notificationExpectation], timeout: .shortTimeout) } - func test_updateCallback() throws { - expectation(forNotification: .embraceConfigUpdated, object: nil) { _ in - return true - } + // MARK: appDidBecomeActive - // given a config with 1 hour minimum update interval - let options = testOptions( - deviceId: TestConstants.deviceId, - minimumUpdateInterval: 60 * 60 - ) + func test_appDidBecomeActive_callsUpdate() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateExpectation.expectedFulfillmentCount = 1 - // Given the response is successful (necessary to save the `lastUpdateTime` value) - try mockSuccessfulResponse() + _ = buildConfig(configurable: mockConfig) - // Given an EmbraceConfig (executes fetch on init) - let config = EmbraceConfig(options: options, notificationCenter: NotificationCenter.default, logger: logger) + NotificationCenter.default.post( + name: NSNotification.Name("UIApplicationDidBecomeActiveNotification"), + object: nil) - // Wait until the fetch from init has finished - wait(timeout: .longTimeout) { - config.updating == false - } + wait(for: [mockConfig.updateExpectation]) + } - // making sure the fetched config is different so the notification is triggered - config.payload.backgroundSessionThreshold = 12345 + func test_appDidBecomeActive_afterUpdate_doesNotCallUpdate() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateExpectation.expectedFulfillmentCount = 1 + let config = buildConfig(configurable: mockConfig) config.update() - waitForExpectations(timeout: .veryLongTimeout) + NotificationCenter.default.post( + name: NSNotification.Name("UIApplicationDidBecomeActiveNotification"), + object: nil) + + wait(for: [mockConfig.updateExpectation]) } - func test_invalidDeviceId() { - // given a config with an invalid device id - let config = EmbraceConfig( - options: testOptions(deviceId: ""), - notificationCenter: NotificationCenter.default, - logger: logger - ) + func test_appDidBecomeActive_afterUpdate_doesCallUpdate_ifMinimumTimePassed() { + let mockConfig = MockEmbraceConfigurable() + mockConfig.updateExpectation.expectedFulfillmentCount = 2 - // then all settings are disabled - XCTAssertFalse(config.isSDKEnabled) - XCTAssertFalse(config.isBackgroundSessionEnabled) - XCTAssertFalse(config.isNetworkSpansForwardingEnabled) - } + let config = buildConfig(configurable: mockConfig, options: .init(minimumUpdateInterval: 0)) - func test_isSDKEnabled() { - // given a config - let config = EmbraceConfig( - options: testOptions(deviceId: TestConstants.deviceId), - notificationCenter: NotificationCenter.default, - logger: logger - ) + let result1 = config.updateIfNeeded() + XCTAssertTrue(result1) - // then isSDKEnabled returns the correct values - config.payload.sdkEnabledThreshold = 100 - XCTAssertTrue(config.isSDKEnabled) + NotificationCenter.default.post( + name: NSNotification.Name("UIApplicationDidBecomeActiveNotification"), + object: nil) - config.payload.sdkEnabledThreshold = 0 - XCTAssertFalse(config.isSDKEnabled) + wait(for: [mockConfig.updateExpectation], timeout: 1) } - func test_isBackgroundSessionEnabled() { - // given a config - let config = EmbraceConfig( - options: testOptions(deviceId: TestConstants.deviceId), - notificationCenter: NotificationCenter.default, - logger: logger - ) +// MARK: Configurable Delegation - // then isBackgroundSessionEnabled returns the correct values - config.payload.backgroundSessionThreshold = 100 - XCTAssertTrue(config.isBackgroundSessionEnabled) + func test_isSDKEnabled_callsUnderlyingConfigurable() { + let mockConfig = MockEmbraceConfigurable() + let config = buildConfig(configurable: mockConfig) - config.payload.backgroundSessionThreshold = 0 - XCTAssertFalse(config.isBackgroundSessionEnabled) + let result = config.isSDKEnabled + XCTAssertEqual(result, mockConfig.isSDKEnabled) + wait(for: [mockConfig.isSDKEnabledExpectation]) } - func test_networkSpansForwardingEnabled() { - // given a config - let config = EmbraceConfig( - options: testOptions(deviceId: TestConstants.deviceId), - notificationCenter: NotificationCenter.default, - logger: logger - ) + func test_isBackgroundSessionEnabled_callsUnderlyingConfigurable() { + let mockConfig = MockEmbraceConfigurable() + let config = buildConfig(configurable: mockConfig) - // then isNetworkSpansForwardingEnabled returns the correct values - config.payload.networkSpansForwardingThreshold = 100 - XCTAssertTrue(config.isNetworkSpansForwardingEnabled) - - config.payload.networkSpansForwardingThreshold = 0 - XCTAssertFalse(config.isNetworkSpansForwardingEnabled) + let result = config.isBackgroundSessionEnabled + XCTAssertEqual(result, mockConfig.isBackgroundSessionEnabled) + wait(for: [mockConfig.isBackgroundSessionEnabledExpectation]) } - func test_internalLogsLimits() { - // given a config - let config = EmbraceConfig( - options: testOptions(deviceId: TestConstants.deviceId), - notificationCenter: NotificationCenter.default, - logger: logger - ) + func test_isNetworkSpansForwardingEnabled_callsUnderlyingConfigurable() { + let mockConfig = MockEmbraceConfigurable() + let config = buildConfig(configurable: mockConfig) - // then internal logs limits returns the correct values - config.payload.internalLogsTraceLimit = 10 - config.payload.internalLogsDebugLimit = 20 - config.payload.internalLogsInfoLimit = 30 - config.payload.internalLogsWarningLimit = 40 - config.payload.internalLogsErrorLimit = 50 - - XCTAssertEqual(config.internalLogsTraceLimit, 10) - XCTAssertEqual(config.internalLogsDebugLimit, 20) - XCTAssertEqual(config.internalLogsInfoLimit, 30) - XCTAssertEqual(config.internalLogsWarningLimit, 40) - XCTAssertEqual(config.internalLogsErrorLimit, 50) + let result = config.isNetworkSpansForwardingEnabled + XCTAssertEqual(result, mockConfig.isNetworkSpansForwardingEnabled) + wait(for: [mockConfig.isNetworkSpansForwardingEnabledExpectation]) } - func test_networkPayloadCaptureRules() { - // given a config - let config = EmbraceConfig( - options: testOptions(deviceId: TestConstants.deviceId), - notificationCenter: NotificationCenter.default, - logger: logger - ) + func test_internalLogLimits_callsUnderlyingConfigurable() { + let mockConfig = MockEmbraceConfigurable() + let config = buildConfig(configurable: mockConfig) - // then network capture rules are returned correctly - let expiration = Date().timeIntervalSince1970 - - config.payload.networkPayloadCaptureRules = [ - NetworkPayloadCaptureRule( - id: "test1", - urlRegex: "test1", - statusCodes: [200], - methods: ["GET"], - expiration: expiration, - publicKey: "key1" - ), - NetworkPayloadCaptureRule( - id: "test2", - urlRegex: "test2", - statusCodes: [500], - methods: ["POST"], - expiration: expiration, - publicKey: "key2" - ) - ] - - XCTAssertEqual(config.networkPayloadCaptureRules.count, 2) - XCTAssertEqual(config.networkPayloadCaptureRules[0].id, "test1") - XCTAssertEqual(config.networkPayloadCaptureRules[1].id, "test2") - } - - func test_hexValue() { - // given an invalid device id - let config1 = EmbraceConfig( - options: testOptions(deviceId: "short"), - notificationCenter: NotificationCenter.default, - logger: logger - ) + let result = config.internalLogLimits + XCTAssertEqual(result, mockConfig.internalLogLimits) + wait(for: [mockConfig.internalLogLimitsExpectation]) + } - // then the internal hex value is defaulted to UInt64.max - // which will make all configs be disabled - XCTAssertEqual(config1.deviceIdHexValue, UInt64.max) + func test_networkPayloadCaptureRules_callsUnderlyingConfigurable() { + let mockConfig = MockEmbraceConfigurable() + let config = buildConfig(configurable: mockConfig) - // given valid device ids - let config2 = EmbraceConfig( - options: testOptions(deviceId: "000000"), - notificationCenter: NotificationCenter.default, - logger: logger - ) - let config3 = EmbraceConfig( - options: testOptions(deviceId: "123456"), - notificationCenter: NotificationCenter.default, - logger: logger) - let config4 = EmbraceConfig( - options: testOptions(deviceId: "ABCDEF"), - notificationCenter: NotificationCenter.default, - logger: logger) - let config5 = EmbraceConfig( - options: testOptions(deviceId: "A5F67E"), - notificationCenter: NotificationCenter.default, - logger: logger) - let config6 = EmbraceConfig( - options: testOptions(deviceId: "FFFFFF"), - notificationCenter: NotificationCenter.default, - logger: logger) - - // then the internal hex values are parsed correctly - XCTAssertEqual(config2.deviceIdHexValue, 0x0) - XCTAssertEqual(config3.deviceIdHexValue, 0x123456) - XCTAssertEqual(config4.deviceIdHexValue, 0xABCDEF) - XCTAssertEqual(config5.deviceIdHexValue, 0xA5F67E) - XCTAssertEqual(config6.deviceIdHexValue, 0xFFFFFF) + let result = config.networkPayloadCaptureRules + XCTAssertEqual(result, mockConfig.networkPayloadCaptureRules) + wait(for: [mockConfig.networkPayloadCaptureRulesExpectation]) } - func test_isEnabled() { - XCTAssertTrue(EmbraceConfig.isEnabled(hexValue: 0xFFFFFF, digits: 6, threshold: 100)) - XCTAssertFalse(EmbraceConfig.isEnabled(hexValue: 0xFFFFFF, digits: 6, threshold: 99)) - XCTAssertFalse(EmbraceConfig.isEnabled(hexValue: 0xFFFFFF, digits: 6, threshold: 0)) - XCTAssertTrue(EmbraceConfig.isEnabled(hexValue: 0, digits: 6, threshold: 100)) - XCTAssertFalse(EmbraceConfig.isEnabled(hexValue: 0, digits: 6, threshold: 0)) - XCTAssert(EmbraceConfig.isEnabled(hexValue: 0x7FFFFF, digits: 6, threshold: 50)) - XCTAssertFalse(EmbraceConfig.isEnabled(hexValue: 0x7FFFFF, digits: 6, threshold: 49)) - } } diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift new file mode 100644 index 00000000..69a0bbf8 --- /dev/null +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigFetcherTests.swift @@ -0,0 +1,172 @@ +// +// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. +// + +import XCTest +import TestSupport +@testable import EmbraceConfigInternal +import EmbraceCommonInternal + +class RemoteConfigFetcherTests: XCTestCase { + static var urlSessionConfig: URLSessionConfiguration! + let logger = MockLogger() + + private var apiBaseUrl: String { + "https://embrace.\(testName).com/config" + } + + override func setUpWithError() throws { + let config = URLSessionConfiguration.ephemeral + config.httpMaximumConnectionsPerHost = .max + Self.urlSessionConfig = config + Self.urlSessionConfig.protocolClasses = [EmbraceHTTPMock.self] + } + + func fetcherOptions( + deviceId: DeviceIdentifier = TestConstants.deviceId, + queue: DispatchQueue = DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent), + appId: String = TestConstants.appId, + osVersion: String = TestConstants.osVersion, + sdkVersion: String = TestConstants.sdkVersion, + appVersion: String = TestConstants.appVersion, + userAgent: String = TestConstants.userAgent + ) -> RemoteConfig.Options { + return RemoteConfig.Options( + apiBaseUrl: apiBaseUrl, + queue: DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent), + appId: appId, + deviceId: deviceId, + osVersion: osVersion, + sdkVersion: sdkVersion, + appVersion: appVersion, + userAgent: userAgent, + urlSessionConfiguration: Self.urlSessionConfig + ) + } + + func mockSuccessfulResponse() throws { + var url = try XCTUnwrap(URL(string: "\(apiBaseUrl)/v2/config")) + + if #available(iOS 16.0, *) { + url.append(queryItems: [ + .init(name: "appId", value: TestConstants.appId), + .init(name: "osVersion", value: TestConstants.osVersion), + .init(name: "appVersion", value: TestConstants.appVersion), + .init(name: "sdkVersion", value: TestConstants.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)) + } + + func mock404Response() throws { + var url = try XCTUnwrap(URL(string: "\(apiBaseUrl)/v2/config")) + + if #available(iOS 16.0, *) { + url.append(queryItems: [ + .init(name: "appId", value: TestConstants.appId), + .init(name: "osVersion", value: TestConstants.osVersion), + .init(name: "appVersion", value: TestConstants.appVersion), + .init(name: "deviceId", value: TestConstants.deviceId.hex), + .init(name: "sdkVersion", value: TestConstants.sdkVersion) + ]) + } else { + XCTFail("This will fail on versions prior to iOS 16.0") + } + + EmbraceHTTPMock.mock(url: url, response: .withData(Data(), statusCode: 404)) + } + + // MARK: buildURL + func test_buildURL_addsCorrectQuery() throws { + let fetcher = RemoteConfigFetcher(options: fetcherOptions(), logger: logger) + let url = try XCTUnwrap(fetcher.buildURL()) + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + + let queryItems = try XCTUnwrap(components?.queryItems) + XCTAssertTrue(queryItems.contains { $0.name == "appId" && $0.value == TestConstants.appId }) + XCTAssertTrue(queryItems.contains { $0.name == "osVersion" && $0.value == TestConstants.osVersion }) + XCTAssertTrue(queryItems.contains { $0.name == "appVersion" && $0.value == TestConstants.appVersion }) + XCTAssertTrue(queryItems.contains { $0.name == "sdkVersion" && $0.value == TestConstants.sdkVersion }) + XCTAssertEqual(queryItems.count, 4) + } + + // MARK: newRequest + func test_newRequest_hasCorrectHeaders() throws { + let fetcher = RemoteConfigFetcher(options: fetcherOptions(), logger: logger) + let request = try XCTUnwrap(fetcher.newRequest()) + + XCTAssertEqual(request.cachePolicy, .useProtocolCachePolicy) + XCTAssertEqual(request.httpMethod, "GET") + + let headers = try XCTUnwrap(request.allHTTPHeaderFields) + XCTAssertEqual(request.value(forHTTPHeaderField: "Accept"), "application/json") + XCTAssertEqual(request.value(forHTTPHeaderField: "User-Agent"), TestConstants.userAgent) + } + + func test_newRequest_addsETagWhenCachedResponsePresent() throws { + let fetcher = RemoteConfigFetcher(options: fetcherOptions(), logger: logger) + let response = HTTPURLResponse( + url: try XCTUnwrap(fetcher.buildURL()), + statusCode: 200, + httpVersion: nil, + headerFields: ["ETag": "stubbed-etag"] + )! + let firstRequest = fetcher.newRequest()! + XCTAssertNil(firstRequest.value(forHTTPHeaderField: "ETag")) + + let cache = try XCTUnwrap(Self.urlSessionConfig.urlCache) + cache.storeCachedResponse( + CachedURLResponse(response: response, data: Data()), + for: firstRequest + ) + + let request = try XCTUnwrap(fetcher.newRequest()) + XCTAssertEqual(request.value(forHTTPHeaderField: "If-None-Match"), "stubbed-etag") + } + + // MARK: fetch + func test_fetch_completesSuccessfullyWithPayload() throws { + // given a config with 1 hour minimum update interval + let options = fetcherOptions() + + // Given the response is successful + try mockSuccessfulResponse() + + // Given an RemoteConfig (executes fetch on init) + let fetcher = RemoteConfigFetcher(options: options, logger: logger) + + let expectation = expectation(description: "URL request") + fetcher.fetch { payload in + XCTAssertNotNil(payload) + expectation.fulfill() + } + wait(for: [expectation]) + } + + func test_fetch_completesFailureWithNilPayload() throws { + // given a config with 1 hour minimum update interval + let options = fetcherOptions() + + // Given the response is successful + try mock404Response() + + // Given an RemoteConfig (executes fetch on init) + let fetcher = RemoteConfigFetcher(options: options, logger: logger) + + let expectation = expectation(description: "URL request") + fetcher.fetch { payload in + XCTAssertNil(payload) + expectation.fulfill() + } + wait(for: [expectation]) + } +} diff --git a/Tests/EmbraceConfigInternalTests/RemoteConfigPayloadTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigPayloadTests.swift similarity index 96% rename from Tests/EmbraceConfigInternalTests/RemoteConfigPayloadTests.swift rename to Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigPayloadTests.swift index 129d49a7..d19d31f8 100644 --- a/Tests/EmbraceConfigInternalTests/RemoteConfigPayloadTests.swift +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfig/RemoteConfigPayloadTests.swift @@ -12,7 +12,7 @@ class RemoteConfigPayloadTests: XCTestCase { func test_defaults() { // given an empty remote config - let path = Bundle.module.path(forResource: "remote_config_empty", ofType: "json", inDirectory: "Mocks")! + let path = Bundle.module.path(forResource: "remote_config_empty", ofType: "json", inDirectory: "Fixtures")! let data = try! Data(contentsOf: URL(fileURLWithPath: path)) // then the default values are used @@ -30,7 +30,7 @@ class RemoteConfigPayloadTests: XCTestCase { func test_values() { // given a valid remote config - let path = Bundle.module.path(forResource: "remote_config", ofType: "json", inDirectory: "Mocks")! + let path = Bundle.module.path(forResource: "remote_config", ofType: "json", inDirectory: "Fixtures")! let data = try! Data(contentsOf: URL(fileURLWithPath: path)) // then the values are correct diff --git a/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift new file mode 100644 index 00000000..c1103ae1 --- /dev/null +++ b/Tests/EmbraceConfigInternalTests/EmbraceConfigurable/RemoteConfigTests.swift @@ -0,0 +1,159 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import XCTest +import TestSupport +@testable import EmbraceConfigInternal +@testable import EmbraceConfiguration +import EmbraceCommonInternal + +final class RemoteConfigTests: XCTestCase { + + let logger = MockLogger() + + let options = RemoteConfig.Options( + apiBaseUrl: "https://localhost:8080/config", + queue: DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent), + appId: TestConstants.appId, + deviceId: DeviceIdentifier(string: "00000000000000000000000000800000")!, // %50 threshold + osVersion: TestConstants.osVersion, + sdkVersion: TestConstants.sdkVersion, + appVersion: TestConstants.appVersion, + userAgent: TestConstants.userAgent, + urlSessionConfiguration: URLSessionConfiguration.default + ) + + func mockSuccessfulResponse() throws { + var url = try XCTUnwrap(URL(string: "\(options.apiBaseUrl)/v2/config")) + + if #available(iOS 16.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() { + // True if threshold 100 + XCTAssertTrue(RemoteConfig.isEnabled(hexValue: 15, digits: 1, threshold: 100.0)) + // False if threshold 0 + XCTAssertFalse(RemoteConfig.isEnabled(hexValue: 0, digits: 1, threshold: 0.0)) + // True if threshold just under (128 limit) + XCTAssertTrue(RemoteConfig.isEnabled(hexValue: 127, digits: 2, threshold: 50.0)) + // False if threshold just over (128 limit) + XCTAssertFalse(RemoteConfig.isEnabled(hexValue: 129, digits: 2, threshold: 50.0)) + } + + func test_isSdkEnabled_usesPayloadThreshold() { + // given a config + let config = RemoteConfig(options: options, logger: logger) + + // then isSDKEnabled returns the correct values + config.payload.sdkEnabledThreshold = 100 + XCTAssertTrue(config.isSDKEnabled) + + config.payload.sdkEnabledThreshold = 0 + XCTAssertFalse(config.isSDKEnabled) + + config.payload.sdkEnabledThreshold = 51 + XCTAssertTrue(config.isSDKEnabled) + + config.payload.sdkEnabledThreshold = 49 + XCTAssertFalse(config.isSDKEnabled) + } + + func test_isBackgroundSessionEnabled() { + // given a config + let config = RemoteConfig(options: options, logger: logger) + + // then isBackgroundSessionEnabled returns the correct values + config.payload.backgroundSessionThreshold = 100 + XCTAssertTrue(config.isBackgroundSessionEnabled) + + config.payload.backgroundSessionThreshold = 0 + XCTAssertFalse(config.isBackgroundSessionEnabled) + + config.payload.backgroundSessionThreshold = 51 + XCTAssertTrue(config.isBackgroundSessionEnabled) + + config.payload.backgroundSessionThreshold = 49 + XCTAssertFalse(config.isBackgroundSessionEnabled) + } + + func test_networkSpansForwardingEnabled() { + // given a config + let config = RemoteConfig(options: options, logger: logger) + + // then isNetworkSpansForwardingEnabled returns the correct values + config.payload.networkSpansForwardingThreshold = 100 + XCTAssertTrue(config.isNetworkSpansForwardingEnabled) + + config.payload.networkSpansForwardingThreshold = 0 + XCTAssertFalse(config.isNetworkSpansForwardingEnabled) + + config.payload.networkSpansForwardingThreshold = 51 + XCTAssertTrue(config.isNetworkSpansForwardingEnabled) + + config.payload.networkSpansForwardingThreshold = 49 + XCTAssertFalse(config.isNetworkSpansForwardingEnabled) + } + + func test_internalLogLimits() { + // given a config + let config = RemoteConfig(options: options, logger: logger) + + config.payload.internalLogsTraceLimit = 10 + config.payload.internalLogsDebugLimit = 20 + config.payload.internalLogsInfoLimit = 30 + config.payload.internalLogsWarningLimit = 40 + config.payload.internalLogsErrorLimit = 50 + + XCTAssertEqual( + config.internalLogLimits, + InternalLogLimits(trace: 10, debug: 20, info: 30, warning: 40, error: 50) + ) + } + + func test_networkPayloadCaptureRules() { + // given a config + let config = RemoteConfig(options: options, logger: logger) + + let rule1 = NetworkPayloadCaptureRule( + id: "test1", + urlRegex: "https://example.com/.*", + statusCodes: [200], + methods: ["GET"], + expiration: 0, + publicKey: "" + ) + + let rule2 = NetworkPayloadCaptureRule( + id: "test2", + urlRegex: "https://test.com/.*", + statusCodes: [404], + methods: ["GET"], + expiration: 0, + publicKey: "" + ) + + config.payload.networkPayloadCaptureRules = [rule1, rule2] + XCTAssertEqual(config.networkPayloadCaptureRules, [rule1, rule2]) + } +} diff --git a/Tests/EmbraceConfigInternalTests/Mocks/remote_config.json b/Tests/EmbraceConfigInternalTests/Fixtures/remote_config.json similarity index 100% rename from Tests/EmbraceConfigInternalTests/Mocks/remote_config.json rename to Tests/EmbraceConfigInternalTests/Fixtures/remote_config.json diff --git a/Tests/EmbraceConfigInternalTests/Mocks/remote_config_empty.json b/Tests/EmbraceConfigInternalTests/Fixtures/remote_config_empty.json similarity index 100% rename from Tests/EmbraceConfigInternalTests/Mocks/remote_config_empty.json rename to Tests/EmbraceConfigInternalTests/Fixtures/remote_config_empty.json diff --git a/Tests/EmbraceConfigInternalTests/RemoteConfigFetcherTests.swift b/Tests/EmbraceConfigInternalTests/RemoteConfigFetcherTests.swift deleted file mode 100644 index 499b4098..00000000 --- a/Tests/EmbraceConfigInternalTests/RemoteConfigFetcherTests.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -import TestSupport -@testable import EmbraceConfigInternal - -// swiftlint:disable force_try - -class RemoteConfigFetcherTests: XCTestCase { - static var urlSessionConfig: URLSessionConfiguration! - - func testOptions(testName: String = #function) -> EmbraceConfig.Options { - return EmbraceConfig.Options( - apiBaseUrl: "https://embrace.\(testName).com", - queue: DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent), - appId: TestConstants.appId, - deviceId: TestConstants.deviceId, - osVersion: TestConstants.osVersion, - sdkVersion: TestConstants.sdkVersion, - appVersion: TestConstants.appVersion, - userAgent: TestConstants.userAgent, - urlSessionConfiguration: RemoteConfigFetcherTests.urlSessionConfig - ) - } - - let logger = MockLogger() - - override func setUpWithError() throws { - // can't use ephemeral because we need to test the cache - RemoteConfigFetcherTests.urlSessionConfig = URLSessionConfiguration.default - RemoteConfigFetcherTests.urlSessionConfig.protocolClasses = [EmbraceHTTPMock.self] - } - - func test_requestMetadata() { - // given a fetcher - let fetcher = RemoteConfigFetcher(options: testOptions(), logger: logger) - - // then requests created are correct - let request = fetcher.newRequest() - - let expectedUrl = "\(testOptions().apiBaseUrl)/v2/config?appId=\(testOptions().appId)&osVersion=\(testOptions().osVersion)&appVersion=\(testOptions().appVersion)&deviceId=\(testOptions().deviceId)&sdkVersion=\(testOptions().sdkVersion)" - XCTAssertEqual(request!.url?.absoluteString, expectedUrl) - XCTAssertEqual(request!.httpMethod, "GET") - XCTAssertEqual(request!.allHTTPHeaderFields!["Accept"], "application/json") - XCTAssertEqual(request!.allHTTPHeaderFields!["User-Agent"], "Embrace/i/\(testOptions().sdkVersion)") - } - - func test_ETag() throws { - // given a fetcher - let fetcher = RemoteConfigFetcher(options: testOptions(), logger: logger) - - // when there's a cached response - let url = try XCTUnwrap(fetcher.buildURL()) - let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["ETag": "test"])! - let firstRequest = fetcher.newRequest()! - - let cache = fetcher.session.configuration.urlCache! - cache.storeCachedResponse( - CachedURLResponse(response: response, data: Data()), - for: firstRequest - ) - - // then the ETag is correctly handled in subsequent requests - let secondRequest = fetcher.newRequest()! - XCTAssertEqual(secondRequest.allHTTPHeaderFields!["If-None-Match"], "test") - } - - func test_fetchSuccess() throws { - // given a fetcher - let fetcher = RemoteConfigFetcher(options: testOptions(), logger: logger) - - // and a valid remote config - let url = try XCTUnwrap(fetcher.buildURL()) - let path = Bundle.module.path(forResource: "remote_config", ofType: "json", inDirectory: "Mocks")! - let data = try! Data(contentsOf: URL(fileURLWithPath: path)) - EmbraceHTTPMock.mock(url: url, data: data) - - // when fetching the config - let expectation = XCTestExpectation() - fetcher.fetch { payload in - // then the payload is valid - XCTAssertNotNil(payload) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .longTimeout) - - XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) - XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(url).count, 1) - } - - func test_fetchSuccess_wrongResponseCode() throws { - // given a fetcher - let fetcher = RemoteConfigFetcher(options: testOptions(), logger: logger) - - // and a valid remote config - let url = try XCTUnwrap(fetcher.buildURL()) - let path = Bundle.module.path(forResource: "remote_config", ofType: "json", inDirectory: "Mocks")! - let data = try! Data(contentsOf: URL(fileURLWithPath: path)) - - // but an invalid response code - EmbraceHTTPMock.mock(url: url, data: data, statusCode: 300) - - // when fetching the config - let expectation = XCTestExpectation() - fetcher.fetch { payload in - // then the payload is not valid - XCTAssertNil(payload) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .longTimeout) - - XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) - XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(url).count, 1) - } - - func test_fetchFailure() throws { - // given a fetcher - let fetcher = RemoteConfigFetcher(options: testOptions(), logger: logger) - - // when failing to fetch the config - let url = try XCTUnwrap(fetcher.buildURL()) - EmbraceHTTPMock.mock(url: url, errorCode: 500) - - let expectation = XCTestExpectation() - fetcher.fetch { payload in - // then the payload is not valid - XCTAssertNil(payload) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .longTimeout) - - XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) - XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(url).count, 1) - } -} - -// swiftlint:enable force_try diff --git a/Tests/EmbraceConfigurationTests/EmbraceConfigurable/DefaultConfigTests.swift b/Tests/EmbraceConfigurationTests/EmbraceConfigurable/DefaultConfigTests.swift new file mode 100644 index 00000000..201659be --- /dev/null +++ b/Tests/EmbraceConfigurationTests/EmbraceConfigurable/DefaultConfigTests.swift @@ -0,0 +1,19 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import XCTest +import EmbraceConfiguration + +final class DefaultConfigTests: XCTestCase { + + func test_defaultConfig_hasCorrectValues() { + let config = DefaultConfig() + + XCTAssertTrue(config.isSDKEnabled) + XCTAssertFalse(config.isBackgroundSessionEnabled) + XCTAssertFalse(config.isNetworkSpansForwardingEnabled) + XCTAssertEqual(config.internalLogLimits, InternalLogLimits()) + XCTAssertTrue(config.networkPayloadCaptureRules.isEmpty) + } +} diff --git a/Tests/EmbraceConfigurationTests/InternalLogLimitsTests.swift b/Tests/EmbraceConfigurationTests/InternalLogLimitsTests.swift new file mode 100644 index 00000000..32a76486 --- /dev/null +++ b/Tests/EmbraceConfigurationTests/InternalLogLimitsTests.swift @@ -0,0 +1,83 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import XCTest +import EmbraceConfiguration + +final class InternalLogLimitsTests: XCTestCase { + + func test_init_hasCorrectDefaultValues() { + let limits = InternalLogLimits() + XCTAssertEqual(limits.trace, 0) + XCTAssertEqual(limits.debug, 0) + XCTAssertEqual(limits.info, 0) + XCTAssertEqual(limits.warning, 0) + XCTAssertEqual(limits.error, 3) + } + + func test_init_withValues() { + let limits = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 5 + ) + XCTAssertEqual(limits.trace, 1) + XCTAssertEqual(limits.debug, 2) + XCTAssertEqual(limits.info, 3) + XCTAssertEqual(limits.warning, 4) + XCTAssertEqual(limits.error, 5) + } + + func test_isEqual_isTrueWhenLimitsMatch() { + let limits1 = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 5 + ) + let limits2 = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 5 + ) + XCTAssertEqual(limits1, limits2) + } + + func test_isEqual_isFalseWhenLimitsDontMatch() { + let limits1 = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 5 + ) + let limits2 = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 6 + ) + XCTAssertNotEqual(limits1, limits2) + } + + func test_isEqual_isFalseWhenDifferentTypes() { + let limits = InternalLogLimits( + trace: 1, + debug: 2, + info: 3, + warning: 4, + error: 5 + ) + + let result = limits.isEqual("InternalLogLimits") + XCTAssertFalse(result) + } + +} diff --git a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLAndCompletionSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLAndCompletionSwizzlerTests.swift index e86dc820..3cd5393d 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLAndCompletionSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLAndCompletionSwizzlerTests.swift @@ -107,7 +107,7 @@ private extension DataTaskWithURLAndCompletionSwizzlerTests { url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) } func givenFailedRequest() { diff --git a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestAndCompletionSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestAndCompletionSwizzlerTests.swift index 7cd26bdf..f1dc9c66 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestAndCompletionSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestAndCompletionSwizzlerTests.swift @@ -105,7 +105,7 @@ private extension DataTaskWithURLRequestAndCompletionSwizzlerTests { request = URLRequest(url: url) let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) } func givenFailedRequest() { diff --git a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestSwizzlerTests.swift index 70729b61..4e0e4815 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLRequestSwizzlerTests.swift @@ -60,7 +60,7 @@ private extension DataTaskWithURLRequestSwizzlerTests { let request = URLRequest(url: url) let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) dataTask = session.dataTask(with: request) dataTask.resume() } diff --git a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLSwizzlerTests.swift index 5c32c6d9..95322c01 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DataTaskWithURLSwizzlerTests.swift @@ -54,7 +54,7 @@ private extension DataTaskWithURLSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) dataTask = session.dataTask(with: url) dataTask.resume() } diff --git a/Tests/EmbraceCoreTests/Capture/Network/DefaultURLSessionTaskHandlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DefaultURLSessionTaskHandlerTests.swift index d8dcd611..0fa02d09 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DefaultURLSessionTaskHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DefaultURLSessionTaskHandlerTests.swift @@ -237,7 +237,7 @@ private extension DefaultURLSessionTaskHandlerTests { request.httpBody = body } let urlResponse = response ?? aValidResponse() - url.mockResponse = .sucessful(withData: UUID().uuidString.data(using: .utf8)!, response: urlResponse) + url.mockResponse = .successful(withData: UUID().uuidString.data(using: .utf8)!, response: urlResponse) task = session.dataTask(with: request) } diff --git a/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestSwizzlerTests.swift index 68d743bb..9edf816e 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestSwizzlerTests.swift @@ -62,7 +62,7 @@ private extension DownloadTaskWithURLRequestSwizzlerTests { let request = URLRequest(url: url) let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) downloadTask = session.downloadTask(with: request) downloadTask.resume() } diff --git a/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestWithCompletionSwizzler.swift b/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestWithCompletionSwizzler.swift index 2bc56237..4e05b827 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestWithCompletionSwizzler.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/DownloadTaskWithURLRequestWithCompletionSwizzler.swift @@ -100,7 +100,7 @@ private extension DownloadTaskWithURLWithCompletionSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) request.httpMethod = "POST" } diff --git a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift index 0438f80c..f2768390 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift @@ -7,6 +7,7 @@ import TestSupport @testable import EmbraceCore @testable import EmbraceConfigInternal import EmbraceStorageInternal +@testable import EmbraceConfiguration class NetworkPayloadCaptureHandlerTests: XCTestCase { diff --git a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRuleTests.swift b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRuleTests.swift index b4f2aa5e..d4f67513 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRuleTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/URLSessionTaskCaptureRuleTests.swift @@ -6,6 +6,7 @@ import XCTest import TestSupport @testable import EmbraceCore @testable import EmbraceConfigInternal +@testable import EmbraceConfiguration import EmbraceStorageInternal class URLSessionTaskCaptureRuleTests: XCTestCase { @@ -72,7 +73,7 @@ class URLSessionTaskCaptureRuleTests: XCTestCase { // given a rule let rule = URLSessionTaskCaptureRule(rule: rule1) - // it should not trigger for a request that doesnt match + // it should not trigger for a request that doesn't match var request = URLRequest(url: URL(string: "www.test.com")!) request.httpMethod = "GET" @@ -84,7 +85,7 @@ class URLSessionTaskCaptureRuleTests: XCTestCase { let rule = URLSessionTaskCaptureRule(rule: rule1) // it should not trigger for a request matches the url - // but doesnt match the method + // but doesn't match the method var request = URLRequest(url: URL(string: "www.test.com/user/1234")!) request.httpMethod = "POST" diff --git a/Tests/EmbraceCoreTests/Capture/Network/Proxy/URLSessionDelegateProxyAsTaskDelegateTests.swift b/Tests/EmbraceCoreTests/Capture/Network/Proxy/URLSessionDelegateProxyAsTaskDelegateTests.swift index 8f7eed7b..105035e1 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/Proxy/URLSessionDelegateProxyAsTaskDelegateTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/Proxy/URLSessionDelegateProxyAsTaskDelegateTests.swift @@ -54,7 +54,7 @@ final class URLSessionDelegateProxyAsTaskDelegateTests: XCTestCase { func mockedURL(string: String, data: Data = Data("Mock Data".utf8)) -> URL { var url = URL(string: string)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: data, response: mockResponse) + url.mockResponse = .successful(withData: data, response: mockResponse) return url } @@ -122,7 +122,7 @@ final class URLSessionDelegateProxyAsTaskDelegateTests: XCTestCase { XCTAssertTrue(try XCTUnwrap(otherSwizzler?.proxy?.didInvokeRespondsTo)) XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didInvokeForwardingTarget)) - XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardToTargetSuccesfully)) + XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardToTargetSuccessfully)) XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardRespondsToSuccessfullyBool)) } @@ -158,7 +158,7 @@ final class URLSessionDelegateProxyAsTaskDelegateTests: XCTestCase { XCTAssertTrue(try XCTUnwrap(otherSwizzler?.proxy?.didInvokeRespondsTo)) XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didInvokeForwardingTarget)) - XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardToTargetSuccesfully)) + XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardToTargetSuccessfully)) XCTAssertFalse(try XCTUnwrap(otherSwizzler?.proxy?.didForwardRespondsToSuccessfullyBool)) } diff --git a/Tests/EmbraceCoreTests/Capture/Network/SessionTaskResumeSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/SessionTaskResumeSwizzlerTests.swift index 1960fa7e..dee51e31 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/SessionTaskResumeSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/SessionTaskResumeSwizzlerTests.swift @@ -50,7 +50,7 @@ private extension SessionTaskResumeSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) _ = try await session.data(from: url) } diff --git a/Tests/EmbraceCoreTests/Capture/Network/URLSessionCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/Network/URLSessionCaptureServiceTests.swift index dec2cfa7..a898a198 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/URLSessionCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/URLSessionCaptureServiceTests.swift @@ -56,7 +56,7 @@ class URLSessionCaptureServiceTests: XCTestCase { givenURLSessionCaptureService() whenInvokingInstall() whenInvokingInstall() - thenEachSwizzlerShoudHaveBeenInstalledOnce() + thenEachSwizzlerShouldHaveBeenInstalledOnce() } } @@ -106,7 +106,7 @@ private extension URLSessionCaptureServiceTests { } } - func thenEachSwizzlerShoudHaveBeenInstalledOnce() { + func thenEachSwizzlerShouldHaveBeenInstalledOnce() { provider.swizzlers.forEach { guard let swizzler = $0 as? MockURLSessionSwizzler else { XCTFail("Swizzler should be a spy") diff --git a/Tests/EmbraceCoreTests/Capture/Network/URLSessionInitWithDelegateSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/URLSessionInitWithDelegateSwizzlerTests.swift index 513659b9..ed2f26b8 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/URLSessionInitWithDelegateSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/URLSessionInitWithDelegateSwizzlerTests.swift @@ -15,7 +15,7 @@ class URLSessionInitWithDelegateSwizzlerTests: XCTestCase { func testAfterInstall_onCreateURLSessionWithDelegate_originalShouldBeWrapped() throws { givenDataTaskWithURLRequestSwizzler() try givenSwizzlingWasDone() - whenInitializingURLSessionWithDummyDelegate() + whenInitializingURLSessionWithDelegate() thenSessionsDelegateShouldntBeDummyDelegate() thenSessionsDelegateShouldBeEmbracesProxy() } @@ -31,6 +31,14 @@ class URLSessionInitWithDelegateSwizzlerTests: XCTestCase { givenDataTaskWithURLRequestSwizzler() thenBaseClassShouldBeURLSession() } + + func test_unsupportedDelegates() throws { + givenDataTaskWithURLRequestSwizzler() + try givenSwizzlingWasDone() + whenInitializingURLSessionWithDelegate(GTMSessionFetcher()) + thenSessionsDelegateShouldntBeEmbracesProxy() + XCTAssertTrue(session.delegate.self is GTMSessionFetcher) + } } private extension URLSessionInitWithDelegateSwizzlerTests { @@ -43,10 +51,9 @@ private extension URLSessionInitWithDelegateSwizzlerTests { try sut.install() } - func whenInitializingURLSessionWithDummyDelegate() { - let originalDelegate = DummyURLSessionDelegate() + func whenInitializingURLSessionWithDelegate(_ delegate: URLSessionDelegate = DummyURLSessionDelegate()) { session = URLSession(configuration: .default, - delegate: originalDelegate, + delegate: delegate, delegateQueue: nil) } @@ -62,7 +69,14 @@ private extension URLSessionInitWithDelegateSwizzlerTests { XCTAssertTrue(session.delegate.self is URLSessionDelegateProxy) } + func thenSessionsDelegateShouldntBeEmbracesProxy() { + XCTAssertFalse(session.delegate.self is URLSessionDelegateProxy) + } + func thenBaseClassShouldBeURLSession() { XCTAssertTrue(sut.baseClass == URLSession.self) } } + +// unsupported delegates +class GTMSessionFetcher: NSObject, URLSessionDelegate {} diff --git a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataAndCompletionSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataAndCompletionSwizzlerTests.swift index 489e8e5d..d3784d5b 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataAndCompletionSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataAndCompletionSwizzlerTests.swift @@ -99,7 +99,7 @@ private extension UploadTaskWithRequestFromDataAndCompletionSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) request.httpMethod = "POST" } diff --git a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataSwizzlerTests.swift index d100b774..44c5ad51 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromDataSwizzlerTests.swift @@ -63,7 +63,7 @@ private extension UploadTaskWithRequestFromDataSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) request.httpMethod = "POST" } diff --git a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileSwizzlerTests.swift index c07a6506..5ba99669 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileSwizzlerTests.swift @@ -63,7 +63,7 @@ private extension UploadTaskWithRequestFromFileSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) request.httpMethod = "POST" } diff --git a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileWithCompletionSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileWithCompletionSwizzlerTests.swift index c34a9147..63d55efc 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileWithCompletionSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithRequestFromFileWithCompletionSwizzlerTests.swift @@ -99,7 +99,7 @@ private extension UploadTaskWithRequestFromFileWithCompletionSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) request.httpMethod = "POST" } diff --git a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithStreamedRequestSwizzlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithStreamedRequestSwizzlerTests.swift index dfd0adba..f101cce6 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithStreamedRequestSwizzlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/UploadTaskWithStreamedRequestSwizzlerTests.swift @@ -64,7 +64,7 @@ private extension UploadTaskWithStreamedRequestSwizzlerTests { var url = URL(string: "https://embrace.io")! let mockData = "Mock Data".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - url.mockResponse = .sucessful(withData: mockData, response: mockResponse) + url.mockResponse = .successful(withData: mockData, response: mockResponse) request = URLRequest(url: url) } diff --git a/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift index d051bf15..7d0c8bae 100644 --- a/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift @@ -13,7 +13,7 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { func test_started() throws { // given an device info capture service let service = DeviceInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb() + let handler = try EmbraceStorage.createInMemoryDb() service.handler = handler // when the service is installed and started @@ -23,6 +23,9 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { // then the app info resources are correctly stored let processId = ProcessIdentifier.current.hex + let resources = try handler.fetchResourcesForProcessId(.current) + XCTAssertEqual(resources.count, 11) + // jailbroken let jailbroken = try handler.fetchMetadata( key: DeviceResourceKey.isJailbroken.rawValue, @@ -123,12 +126,22 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { ) XCTAssertNotNil(osName) XCTAssertEqual(try XCTUnwrap(osName?.stringValue), EMBDevice.operatingSystemType) + + // osName + let architecture = try handler.fetchMetadata( + key: DeviceResourceKey.architecture.rawValue, + type: .requiredResource, + lifespan: .process, + lifespanId: processId + ) + XCTAssertNotNil(architecture) + XCTAssertEqual(try XCTUnwrap(architecture?.stringValue), EMBDevice.architecture) } func test_notStarted() throws { // given an app info capture service let service = AppInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb() + let handler = try EmbraceStorage.createInMemoryDb() service.handler = handler // when the service is installed but not started diff --git a/Tests/EmbraceCoreTests/Capture/System/LowPowerModeCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/System/LowPowerModeCaptureServiceTests.swift index 9d559a76..20365b77 100644 --- a/Tests/EmbraceCoreTests/Capture/System/LowPowerModeCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/System/LowPowerModeCaptureServiceTests.swift @@ -90,7 +90,7 @@ class LowPowerModeCollectorTests: XCTestCase { // when low power mode changes provider.isLowPowerModeEnabled = true - // then it is captued + // then it is captured XCTAssertNotNil(service.currentSpan) } @@ -114,7 +114,7 @@ class LowPowerModeCollectorTests: XCTestCase { // when low power mode changes provider.isLowPowerModeEnabled = true - // then it is not captued + // then it is not captured XCTAssertNil(service.currentSpan) } @@ -176,7 +176,7 @@ class LowPowerModeCollectorTests: XCTestCase { let span = service.currentSpan XCTAssertNotNil(span) - // when the service is stoped + // when the service is stopped service.stop() // then the span is ended @@ -197,7 +197,7 @@ class LowPowerModeCollectorTests: XCTestCase { let span = service.currentSpan XCTAssertNotNil(span) - // when the service is stoped + // when the service is stopped service.stop() // then the span is ended diff --git a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift index ce496201..e2c43bb1 100644 --- a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift @@ -6,6 +6,8 @@ import XCTest @testable import EmbraceCore import TestSupport import EmbraceStorageInternal +import EmbraceConfigInternal +import EmbraceConfiguration import OpenTelemetryApi class DefaultInternalLoggerTests: XCTestCase { diff --git a/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift b/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift index f115d46d..ffa182be 100644 --- a/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift @@ -17,7 +17,7 @@ class DeviceIdentifier_PersistenceTests: XCTestCase { KeychainAccess.keychain = AlwaysSuccessfulKeychainInterface() // delete the resource if we already have it - if let resource = try storage.fetchRequriedPermanentResource(key: DeviceIdentifier.resourceKey) { + if let resource = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { try storage.delete(record: resource) } } @@ -29,7 +29,7 @@ class DeviceIdentifier_PersistenceTests: XCTestCase { func test_retrieve_withNoRecordInStorage_shouldCreateNewPermanentRecord() throws { let result = DeviceIdentifier.retrieve(from: storage) - let resourceRecord = try storage.fetchRequriedPermanentResource(key: DeviceIdentifier.resourceKey) + let resourceRecord = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) XCTAssertNotNil(resourceRecord) XCTAssertEqual(resourceRecord?.lifespan, .permanent) @@ -40,7 +40,7 @@ class DeviceIdentifier_PersistenceTests: XCTestCase { func test_retrieve_withNoRecordInStorage_shouldRequestFromKeychain() throws { // because of our setup we could assume there is no database entry but lets make sure // to delete the resource if we already have it - if let resource = try storage.fetchRequriedPermanentResource(key: DeviceIdentifier.resourceKey) { + if let resource = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { try storage.delete(record: resource) } let keychainDeviceId = KeychainAccess.deviceId diff --git a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift index 518b081d..3bb0a535 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift @@ -11,7 +11,7 @@ import EmbraceCommonInternal class EmbraceLogAttributesBuilderTests: XCTestCase { private var sut: EmbraceLogAttributesBuilder! private var storage: MockMetadataFetcher! - private var controller: SpySessionController! + private var controller: MockSessionController! private var result: [String: String]! // MARK: - Test Build Alone @@ -177,11 +177,12 @@ private extension EmbraceLogAttributesBuilderTests { sessionWithId sessionId: SessionIdentifier = .random, sessionState: SessionState = .foreground ) { - controller = SpySessionController(currentSession: .with(id: sessionId, state: sessionState)) + controller = MockSessionController() + controller.currentSession = .with(id: sessionId, state: sessionState) } func givenSessionControllerWithNoSession() { - controller = SpySessionController(currentSession: nil) + controller = MockSessionController() } func givenMetadataFetcher(with metadata: [MetadataRecord]? = nil) { diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift index 5114d8f9..bb512407 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift @@ -15,21 +15,21 @@ class DefaultLogBatcherTests: XCTestCase { func test_addLog_alwaysTriesToCreateLogInRepository() { givenDefaultLogBatcher() - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenLogRepositoryCreateMethodWasInvoked() } func testOnSuccessfulRepository_whenInvokingAddLog_thenBatchShouldntFinish() { givenDefaultLogBatcher() - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldntInvokeBatchFinished() } func testOnSuccessfulRepository_whenInvokingAddLogMoreTimesThanLimit_thenBatchShouldFinish() { givenDefaultLogBatcher(limits: .init(maxLogsPerBatch: 1)) - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinished() @@ -37,14 +37,14 @@ class DefaultLogBatcherTests: XCTestCase { func testAutoEndBatchAfterLifespanExpired() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 10)) - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinishedAfterBatchLifespan(0.2) } func testAutoEndBatchAfterLifespanExpired_TimerStartsAgainAfterNewLogAdded() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 10)) - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinishedAfterBatchLifespan(0.2) self.delegate.didCallBatchFinished = false @@ -54,7 +54,7 @@ class DefaultLogBatcherTests: XCTestCase { func testAutoEndBatchAfterLifespanExpired_CancelWhenBatchEndedPrematurely() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 3)) - givenRepositoryCreatesLogsSucessfully() + givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) @@ -71,7 +71,7 @@ private extension DefaultLogBatcherTests { sut = .init(repository: repository, logLimits: limits, delegate: delegate, processorQueue: .main) } - func givenRepositoryCreatesLogsSucessfully(withLog log: LogRecord? = nil) { + func givenRepositoryCreatesLogsSuccessfully(withLog log: LogRecord? = nil) { let logRecord = log ?? randomLogRecord() repository.stubbedCreateCompletionResult = .success(logRecord) } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift index 86b9196c..f5226902 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift @@ -61,7 +61,13 @@ class StorageEmbraceLogExporterTests: XCTestCase { func test_havingActiveLogExporter_onExport_whenInvalidBody_exportSucceedsButNotAddedToBatch() { givenStorageEmbraceLogExporter(initialState: .active) - whenInvokingExport(withLogs: [randomLogData(body: nil)]) + + var str = "" + for _ in 1...4001 { + str += "." + } + whenInvokingExport(withLogs: [randomLogData(body: str)]) + thenBatchAdded(count: 0) thenResult(is: .success) } @@ -77,7 +83,12 @@ class StorageEmbraceLogExporterTests: XCTestCase { func test_havingActiveLogExporter_onExportManyLogs_someValidSomeInvalid_shouldInvokeBatcherForEveryValidLog() { let validAmount = Int.random(in: 1..<10) let validLogs = randomLogs(quantity: validAmount, body: "example") - let invalidLogs = randomLogs(quantity: Int.random(in: 1..<10)) + + var str = "" + for _ in 1...4001 { + str += "." + } + let invalidLogs = randomLogs(quantity: Int.random(in: 1..<10), body: str) givenStorageEmbraceLogExporter(initialState: .active) whenInvokingExport(withLogs: (validLogs + invalidLogs).shuffled()) @@ -108,6 +119,12 @@ class StorageEmbraceLogExporterTests: XCTestCase { thenBatchAdded(count: 0) thenResult(is: .success) } + + func test_endBatch_onSessionEnd() { + givenStorageEmbraceLogExporter(initialState: .active) + whenSessionEnds() + thenBatchRenewed() + } } private extension StorageEmbraceLogExporterTests { @@ -128,6 +145,10 @@ private extension StorageEmbraceLogExporterTests { result = sut.forceFlush() } + func whenSessionEnds() { + NotificationCenter.default.post(name: .embraceSessionWillEnd, object: nil) + } + func thenState(is newState: StorageEmbraceLogExporter.State) { XCTAssertEqual(sut.state, newState) } @@ -159,16 +180,27 @@ private extension StorageEmbraceLogExporterTests { randomLogData(body: body) } } + + func thenBatchRenewed() { + XCTAssert(batcher.didCallRenewBatch) + } } class SpyLogBatcher: LogBatcher { - private (set) var didCallAddLogRecord: Bool = false - private (set) var addLogRecordInvocationCount: Int = 0 - private (set) var logRecords = [LogRecord]() + private(set) var didCallAddLogRecord: Bool = false + private(set) var addLogRecordInvocationCount: Int = 0 + private(set) var logRecords = [LogRecord]() func addLogRecord(logRecord: LogRecord) { didCallAddLogRecord = true addLogRecordInvocationCount += 1 logRecords.append(logRecord) } + + private(set) var didCallRenewBatch: Bool = false + private(set) var renewBatchInvocationCount: Int = 0 + func renewBatch(withLogs logRecords: [LogRecord]) { + didCallRenewBatch = true + renewBatchInvocationCount += 1 + } } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidatorTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidatorTests.swift index abfdf622..02f32557 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidatorTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/Validation/Validators/LengthOfBodyValidatorTests.swift @@ -21,14 +21,19 @@ final class LengthOfBodyValidatorTests: XCTestCase { func test_validate_init_defaultsToCorrect_allowedCharacterCount() { let validator = LengthOfBodyValidator() - XCTAssertEqual(validator.allowedCharacterCount, 1...4000) + XCTAssertEqual(validator.allowedCharacterCount, 0...4000) } - func test_validate_isInvalid_ifBodyIsNil() { + func test_validate_isInvalid_ifBodySizeIsOutOfRange() { let validator = LengthOfBodyValidator() - var invalidNil = logData(body: nil) - let result = validator.validate(data: &invalidNil) + var str = "" + for _ in 1...4001 { + str += "." + } + + var invalidLog = logData(body: str) + let result = validator.validate(data: &invalidLog) XCTAssertFalse(result) } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift index 4734ec64..a9f6a893 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift @@ -12,7 +12,7 @@ import EmbraceCommonInternal class LogControllerTests: XCTestCase { private var sut: LogController! private var storage: SpyStorage? - private var sessionController: SpySessionController! + private var sessionController: MockSessionController! private var upload: SpyEmbraceLogUploader! override func setUp() { @@ -49,25 +49,39 @@ class LogControllerTests: XCTestCase { } func testHavingLogs_onSetup_fetchesResourcesFromStorage() throws { - givenStorage(withLogs: [randomLogRecord()]) + let sessionId = SessionIdentifier.random + let log = randomLogRecord(sessionId: sessionId) + + givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - try thenFetchesResourcesForCurrentSessionIdFromStorage() + try thenFetchesResourcesFromStorage(sessionId: sessionId) } func testHavingLogs_onSetup_fetchesMetadataFromStorage() throws { - givenStorage(withLogs: [randomLogRecord()]) + let sessionId = SessionIdentifier.random + let log = randomLogRecord(sessionId: sessionId) + + givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - try thenFetchesMetadataForCurrentSessionIdFromStorage() + try thenFetchesMetadataFromStorage(sessionId: sessionId) } - func testHavingLogsButNoSession_onSetup_wontTryToUploadAnything() { - givenSessionControllerWithoutSession() - givenStorage(withLogs: [randomLogRecord()]) + func testHavingLogsWithNoSessionId_onSetup_fetchesResourcesFromStorage() throws { + let log = randomLogRecord() + givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - thenDoesntTryToUploadAnything() + try thenFetchesResourcesFromStorage(processId: log.processIdentifier) + } + + func testHavingLogsWithNoSessionId_onSetup_fetchesMetadataFromStorage() throws { + let log = randomLogRecord() + givenStorage(withLogs: [log]) + givenLogController() + whenInvokingSetup() + try thenFetchesMetadataFromStorage(processId: log.processIdentifier) } func testHavingLogsForLessThanABatch_onSetup_logUploaderShouldSendASingleBatch() { @@ -112,22 +126,28 @@ class LogControllerTests: XCTestCase { thenDoesntTryToUploadAnything() } + func testHavingSessionButNoLogs_onBatchFinished_wontTryToUploadAnything() { + givenLogController() + whenInvokingBatchFinished(withLogs: []) + thenDoesntTryToUploadAnything() + } + func testHavingLogs_onBatchFinished_fetchesResourcesFromStorage() throws { givenLogController() whenInvokingBatchFinished(withLogs: [randomLogRecord()]) - try thenFetchesResourcesForCurrentSessionIdFromStorage() + try thenFetchesResourcesFromStorage(sessionId: sessionController.currentSession?.id) } func testHavingLogs_onBatchFinished_fetchesMetadataFromStorage() throws { givenLogController() whenInvokingBatchFinished(withLogs: [randomLogRecord()]) - try thenFetchesMetadataForCurrentSessionIdFromStorage() + try thenFetchesMetadataFromStorage(sessionId: sessionController.currentSession?.id) } func testHavingLogs_onBatchFinished_logUploaderShouldSendASingleBatch() throws { givenLogController() whenInvokingBatchFinished(withLogs: [randomLogRecord()]) - try thenFetchesMetadataForCurrentSessionIdFromStorage() + try thenFetchesMetadataFromStorage(sessionId: sessionController.currentSession?.id) } func testHavingThrowingStorage_onBatchFinished_wontTryToUploadAnything() { @@ -250,29 +270,45 @@ private extension LogControllerTests { XCTAssertFalse(unwrappedStorage.didCallRemoveLogs) } - func thenFetchesResourcesForCurrentSessionIdFromStorage() throws { + func thenFetchesResourcesFromStorage(sessionId: SessionIdentifier?) throws { let unwrappedStorage = try XCTUnwrap(storage) - let currentSessionId = sessionController.currentSession?.id XCTAssertTrue(unwrappedStorage.didCallFetchResourcesForSessionId) - XCTAssertEqual(unwrappedStorage.fetchResourcesForSessionIdReceivedParameter, currentSessionId) + XCTAssertEqual(unwrappedStorage.fetchResourcesForSessionIdReceivedParameter, sessionId) } - func thenFetchesMetadataForCurrentSessionIdFromStorage() throws { + func thenFetchesMetadataFromStorage(sessionId: SessionIdentifier?) throws { let unwrappedStorage = try XCTUnwrap(storage) - let currentSessionId = sessionController.currentSession?.id XCTAssertTrue(unwrappedStorage.didCallFetchCustomPropertiesForSessionId) - XCTAssertEqual(unwrappedStorage.fetchCustomPropertiesForSessionIdReceivedParameter, currentSessionId) + XCTAssertEqual(unwrappedStorage.fetchCustomPropertiesForSessionIdReceivedParameter, sessionId) XCTAssertTrue(unwrappedStorage.didCallFetchCustomPropertiesForSessionId) - XCTAssertEqual(unwrappedStorage.fetchPersonaTagsForSessionIdReceivedParameter, currentSessionId) + XCTAssertEqual(unwrappedStorage.fetchPersonaTagsForSessionIdReceivedParameter, sessionId) + } + + func thenFetchesResourcesFromStorage(processId: ProcessIdentifier) throws { + let unwrappedStorage = try XCTUnwrap(storage) + XCTAssertTrue(unwrappedStorage.didCallFetchResourcesForProcessId) + XCTAssertEqual(unwrappedStorage.fetchResourcesForProcessIdReceivedParameter, processId) + } + + func thenFetchesMetadataFromStorage(processId: ProcessIdentifier) throws { + let unwrappedStorage = try XCTUnwrap(storage) + XCTAssertTrue(unwrappedStorage.didCallFetchPersonaTagsForProcessId) + XCTAssertEqual(unwrappedStorage.fetchPersonaTagsForProcessIdReceivedParameter, processId) } - func randomLogRecord() -> LogRecord { - .init( + func randomLogRecord(sessionId: SessionIdentifier? = nil) -> LogRecord { + + var attributes: [String: PersistableValue] = [:] + if let sessionId = sessionId { + attributes["emb.session_id"] = PersistableValue(sessionId.toString) + } + + return LogRecord( identifier: .random, processIdentifier: .random, severity: .info, body: UUID().uuidString, - attributes: .empty() + attributes: attributes ) } diff --git a/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json b/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json new file mode 100644 index 00000000..29d2a3f0 --- /dev/null +++ b/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json @@ -0,0 +1,5 @@ +{ + "background": { + "threshold": 100 + } +} diff --git a/Tests/EmbraceCoreTests/Options/Embrace+OptionsTests.swift b/Tests/EmbraceCoreTests/Options/Embrace+OptionsTests.swift index c9a4d916..41698f9b 100644 --- a/Tests/EmbraceCoreTests/Options/Embrace+OptionsTests.swift +++ b/Tests/EmbraceCoreTests/Options/Embrace+OptionsTests.swift @@ -4,6 +4,9 @@ import XCTest import EmbraceCore +import EmbraceConfigInternal +import EmbraceConfiguration +import TestSupport final class Embrace_OptionsTests: XCTestCase { @@ -25,4 +28,17 @@ final class Embrace_OptionsTests: XCTestCase { XCTAssertEqual(options.export, export) XCTAssertNil(options.appId) } + + func test_init_withRuntimeConfiguration_usesInjectedObject() throws { + let mockObj = MockEmbraceConfigurable() + + let options = Embrace.Options( + export: OpenTelemetryExport(), + captureServices: [], + crashReporter: nil, + runtimeConfiguration: mockObj + ) + + XCTAssertTrue(mockObj === options.runtimeConfiguration) + } } diff --git a/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift b/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift index 9302ebce..b1b93cbf 100644 --- a/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift +++ b/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift @@ -20,6 +20,8 @@ class ResourcePayloadTests: XCTestCase { MetadataRecord.userMetadata(key: AppResourceKey.sdkVersion.rawValue, value: "3.2.1"), MetadataRecord.userMetadata(key: AppResourceKey.processIdentifier.rawValue, value: "12345"), MetadataRecord.userMetadata(key: AppResourceKey.buildID.rawValue, value: "fakebuilduuidnohyphen"), + MetadataRecord.userMetadata(key: AppResourceKey.processStartTime.rawValue, value: "12345"), + MetadataRecord.userMetadata(key: AppResourceKey.processPreWarm.rawValue, value: "true"), // Device Resources that should be present MetadataRecord.createResourceRecord(key: DeviceResourceKey.isJailbroken.rawValue, value: "true"), @@ -58,6 +60,8 @@ class ResourcePayloadTests: XCTestCase { XCTAssertEqual(json["sdk_version"] as? String, "3.2.1") XCTAssertEqual(json["app_version"] as? String, "1.2.3") XCTAssertEqual(json["process_identifier"] as? String, "12345") + XCTAssertEqual(json["process_start_time"] as? Int, 12345) + XCTAssertEqual(json["process_pre_warm"] as? Bool, true) XCTAssertEqual(json["jailbroken"] as? Bool, true) XCTAssertEqual(json["disk_total_capacity"] as? Int, 494384795648) diff --git a/Tests/EmbraceCoreTests/Payload/UserInfoPayloadTests.swift b/Tests/EmbraceCoreTests/Payload/UserInfoPayloadTests.swift deleted file mode 100644 index 019a8e8d..00000000 --- a/Tests/EmbraceCoreTests/Payload/UserInfoPayloadTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceCore -import EmbraceStorageInternal - -final class UserInfoPayloadTests: XCTestCase { - - func test_userinfo_whenNothingSet_isEmpty() throws { - let payload = UserInfoPayload(with: []) - XCTAssertNil(payload.username) - XCTAssertNil(payload.identifier) - XCTAssertNil(payload.email) - } - - func test_userinfo_whenNothingSet_encodesToEmptyJSON() throws { - let payload = UserInfoPayload(with: []) - - let data = try JSONEncoder().encode(payload) - let string = String(data: data, encoding: .utf8) - - XCTAssertEqual(string, #"{}"#) - } - - func test_userinfo_whenUsernameSet_encodesToJSON() throws { - let payload = UserInfoPayload(with: [ - MetadataRecord( - key: UserResourceKey.name.rawValue, - value: .string("test"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ) - ]) - - let data = try JSONEncoder().encode(payload) - let string = String(data: data, encoding: .utf8) - - XCTAssertEqual(string, #"{"un":"test"}"#) - } - - func test_userinfo_whenEmailSet_encodesToJSON() throws { - let payload = UserInfoPayload(with: [ - MetadataRecord( - key: UserResourceKey.email.rawValue, - value: .string("get@me.org"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ) - ]) - - let data = try JSONEncoder().encode(payload) - let string = String(data: data, encoding: .utf8) - - XCTAssertEqual(string, #"{"em":"get@me.org"}"#) - } - - func test_userinfo_whenIdentifierSet_encodesToJSON() throws { - let payload = UserInfoPayload(with: [ - MetadataRecord( - key: UserResourceKey.identifier.rawValue, - value: .string("1234"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ) - ]) - - let data = try JSONEncoder().encode(payload) - let string = String(data: data, encoding: .utf8) - - XCTAssertEqual(string, #"{"id":"1234"}"#) - } - - func test_userinfo_whenEverythingSet_encodesToJSON() throws { - let payload = UserInfoPayload(with: [ - MetadataRecord( - key: UserResourceKey.name.rawValue, - value: .string("test"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ), - MetadataRecord( - key: UserResourceKey.email.rawValue, - value: .string("get@me.org"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ), - MetadataRecord( - key: UserResourceKey.identifier.rawValue, - value: .string("1234"), - type: .customProperty, - lifespan: .permanent, - lifespanId: "" - ) - ]) - - let data = try JSONEncoder().encode(payload) - - let decoded = try JSONDecoder().decode([String: String].self, from: data) - XCTAssertEqual(decoded["em"], "get@me.org") - XCTAssertEqual(decoded["un"], "test") - XCTAssertEqual(decoded["id"], "1234") - } -} diff --git a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift index 1fc360f2..07bee3ad 100644 --- a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift +++ b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift @@ -20,7 +20,7 @@ final class EmbraceCoreTests: XCTestCase { let sessionId = embrace?.currentSessionId() // concurrentPerform performs concurrent operations in a synchronous manner on the called thread. - // so it seems to be good for testing as it prevents the requried + // so it seems to be good for testing as it prevents the required // use of expectations DispatchQueue.concurrentPerform(iterations: 100) {_ in let cSessionId = embrace?.currentSessionId() @@ -34,7 +34,7 @@ final class EmbraceCoreTests: XCTestCase { try embrace?.start() // concurrentPerform performs concurrent operations in a synchronous manner on the called thread. - // so it seems to be good for testing as it prevents the requried + // so it seems to be good for testing as it prevents the required // use of expectations DispatchQueue.concurrentPerform(iterations: 100) {_ in let id = embrace?.currentSessionId() @@ -47,7 +47,7 @@ final class EmbraceCoreTests: XCTestCase { let embrace = try getLocalEmbrace() try embrace?.start() // concurrentPerform performs concurrent operations in a synchronous manner on the called thread. - // so it seems to be good for testing as it prevents the requried + // so it seems to be good for testing as it prevents the required // use of expectations DispatchQueue.concurrentPerform(iterations: 100) {_ in let id = embrace?.currentSessionId() @@ -56,13 +56,13 @@ final class EmbraceCoreTests: XCTestCase { } } - func test_CuncurrentEndSession() throws { + func test_ConcurrentEndSession() throws { let embrace = try getLocalEmbrace() try embrace?.start() let sessionId = embrace?.currentSessionId() // concurrentPerform performs concurrent operations in a synchronous manner on the called thread. - // so it seems to be good for testing as it prevents the requried + // so it seems to be good for testing as it prevents the required // use of expectations DispatchQueue.concurrentPerform(iterations: 100) {_ in embrace?.endCurrentSession() @@ -76,13 +76,13 @@ final class EmbraceCoreTests: XCTestCase { XCTAssertNotEqual(cSessionId, sessionId) } - func test_CuncurrentStartSession() throws { + func test_ConcurrentStartSession() throws { let embrace = try getLocalEmbrace() try embrace?.start() let sessionId = embrace?.currentSessionId() // concurrentPerform performs concurrent operations in a synchronous manner on the called thread. - // so it seems to be good for testing as it prevents the requried + // so it seems to be good for testing as it prevents the required // use of expectations DispatchQueue.concurrentPerform(iterations: 100) {_ in embrace?.startNewSession() @@ -235,10 +235,19 @@ final class EmbraceCoreTests: XCTestCase { func getLocalEmbrace(storage: EmbraceStorage? = nil, crashReporter: CrashReporter? = nil) throws -> Embrace? { // to ensure that each test gets it's own instance of embrace. return try lock.locked { + + // use fake endpoints + let endpoints = Embrace.Endpoints( + baseURL: "https://embrace.\(testName).com/api", + developmentBaseURL: "https://embrace.\(testName).com/api-dev", + configBaseURL: "https://embrace.\(testName).com/config" + ) + // I use random string for group id to ensure a different storage location each time try Embrace.client = Embrace(options: .init( appId: "testA", appGroupId: randomString(length: 5), + endpoints: endpoints, captureServices: [], crashReporter: crashReporter ), embraceStorage: storage) diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift index 02baae4e..97849062 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift @@ -210,7 +210,7 @@ final class MetadataHandlerTests: XCTestCase { // start new session let newSession = sessionController.startSession(state: .foreground) - let secondSessionId = newSession.id + let secondSessionId = newSession!.id try storage.addSession( id: secondSessionId, state: .foreground, diff --git a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift index db63732e..92416d00 100644 --- a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift @@ -9,6 +9,7 @@ import EmbraceStorageInternal @testable import EmbraceUploadInternal import EmbraceCommonInternal import EmbraceOTelInternal +import EmbraceConfigInternal import TestSupport final class SessionControllerTests: XCTestCase { @@ -17,9 +18,6 @@ final class SessionControllerTests: XCTestCase { var controller: SessionController! var upload: EmbraceUpload! - static let testCacheOptions = EmbraceUpload.CacheOptions( - cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()) - )! static let testMetadataOptions = EmbraceUpload.MetadataOptions( apiKey: "apiKey", userAgent: "userAgent", @@ -27,33 +25,30 @@ final class SessionControllerTests: XCTestCase { ) static let testRedundancyOptions = EmbraceUpload.RedundancyOptions(automaticRetryCount: 0) - var testOptions: EmbraceUpload.Options! + var uploadTestOptions: EmbraceUpload.Options! + var queue: DispatchQueue! var module: EmbraceUpload! override func setUpWithError() throws { - if FileManager.default.fileExists(atPath: Self.testCacheOptions.cacheFilePath) { - try FileManager.default.removeItem(atPath: Self.testCacheOptions.cacheFilePath) - } - - let urlSessionconfig = URLSessionConfiguration.ephemeral - urlSessionconfig.httpMaximumConnectionsPerHost = .max - urlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] + let uploadUrlSessionconfig = URLSessionConfiguration.ephemeral + uploadUrlSessionconfig.httpMaximumConnectionsPerHost = .max + uploadUrlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] - testOptions = EmbraceUpload.Options( + uploadTestOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(testName: testName), - cache: Self.testCacheOptions, + cache: EmbraceUpload.CacheOptions(named: testName), metadata: Self.testMetadataOptions, redundancy: Self.testRedundancyOptions, - urlSessionConfiguration: urlSessionconfig + urlSessionConfiguration: uploadUrlSessionconfig ) - self.queue = DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent) - upload = try EmbraceUpload(options: testOptions, logger: MockLogger(), queue: queue) + self.queue = DispatchQueue(label: "com.test.embrace.upload.queue", attributes: .concurrent) + upload = try EmbraceUpload(options: uploadTestOptions, logger: MockLogger(), queue: queue) storage = try EmbraceStorage.createInMemoryDb() - // we pass nil so we only use the upload module in the relevant tests - controller = SessionController(storage: storage, upload: nil) + // we pass nil so we only use the upload/config module in the relevant tests + controller = SessionController(storage: storage, upload: nil, config: nil) } override func tearDownWithError() throws { @@ -67,19 +62,14 @@ final class SessionControllerTests: XCTestCase { let b = controller.startSession(state: .foreground) let c = controller.startSession(state: .foreground) - XCTAssertNotEqual(a.id, b.id) - XCTAssertNotEqual(a.id, c.id) - XCTAssertNotEqual(b.id, c.id) + XCTAssertNotEqual(a!.id, b!.id) + XCTAssertNotEqual(a!.id, c!.id) + XCTAssertNotEqual(b!.id, c!.id) } func test_startSession_setsForegroundState() throws { let a = controller.startSession(state: .foreground) - XCTAssertEqual(a.state, "foreground") - } - - func test_startSession_setsBackgroundState() throws { - let a = controller.startSession(state: .background) - XCTAssertEqual(a.state, "background") + XCTAssertEqual(a!.state, "foreground") } // MARK: startSession @@ -87,7 +77,7 @@ final class SessionControllerTests: XCTestCase { func test_startSession_setsCurrentSession_andPostsDidStartNotification() throws { let notificationExpectation = expectation(forNotification: .embraceSessionDidStart, object: nil) - let session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground)! XCTAssertNotNil(session.startTime) XCTAssertNotNil(controller.currentSessionSpan) XCTAssertEqual(controller.currentSession?.id, session.id) @@ -99,48 +89,15 @@ final class SessionControllerTests: XCTestCase { XCTAssertTrue(controller.currentSession!.coldStart) } - func test_startSession_ifStartAtMatchesAllowedColdStartInterval_marksSessionAsColdStartTrue() throws { - let processStart = ProcessMetadata.startTime! - let startTime = processStart.addingTimeInterval(SessionController.allowedColdStartInterval) - - controller.startSession(state: .foreground, startTime: startTime) - XCTAssertTrue(controller.currentSession!.coldStart) - } - - func test_startSession_ifStartAtIsPassedAllowedColdStartInterval_marksSessionAsColdStartFalse() throws { - let processStart = ProcessMetadata.startTime! - let startTime = processStart.addingTimeInterval(SessionController.allowedColdStartInterval + 1) - - controller.startSession(state: .foreground, startTime: startTime) - XCTAssertFalse(controller.currentSession!.coldStart) - } - - func test_startSession_ifStartAtIsBeforeProcessStart_marksSessionAsColdStartFalse() throws { - let processStart = ProcessMetadata.startTime! - let startTime = processStart.addingTimeInterval(-1) - - controller.startSession(state: .foreground, startTime: startTime) - XCTAssertFalse(controller.currentSession!.coldStart) - } - func test_startSession_saves_foregroundSession() throws { let session = controller.startSession(state: .foreground) let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") } - func test_startSession_saves_backgroundSession() throws { - let session = controller.startSession(state: .background) - - let sessions: [SessionRecord] = try storage.fetchAll() - XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) - XCTAssertEqual(sessions.first?.state, "background") - } - func test_startSession_startsSessionSpan() throws { let spanProcessor = MockSpanProcessor() EmbraceOTel.setup(spanProcessors: [spanProcessor]) @@ -150,7 +107,7 @@ final class SessionControllerTests: XCTestCase { if let spanData = spanProcessor.startedSpans.first { XCTAssertEqual( spanData.startTime.timeIntervalSince1970, - session.startTime.timeIntervalSince1970, + session!.startTime.timeIntervalSince1970, accuracy: 0.001 ) XCTAssertFalse(spanData.hasEnded) @@ -159,6 +116,16 @@ final class SessionControllerTests: XCTestCase { } } + func test_startSession_onlyFirstOneIsColdStart() throws { + var session = controller.startSession(state: .foreground) + XCTAssertTrue(session!.coldStart) + + for _ in 1...10 { + session = controller.startSession(state: .foreground) + XCTAssertFalse(session!.coldStart) + } + } + // MARK: endSession func test_endSession_setsCurrentSessionToNil_andPostsWillEndNotification() throws { @@ -166,7 +133,7 @@ final class SessionControllerTests: XCTestCase { let session = controller.startSession(state: .foreground) XCTAssertNotNil(controller.currentSessionSpan) - XCTAssertNil(session.endTime) + XCTAssertNil(session!.endTime) let endTime = controller.endSession() @@ -182,13 +149,13 @@ final class SessionControllerTests: XCTestCase { func test_endSession_saves_foregroundSession() throws { let session = controller.startSession(state: .foreground) - XCTAssertNil(session.endTime) + XCTAssertNil(session!.endTime) let endTime = controller.endSession() let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first!.id, session.id) + XCTAssertEqual(sessions.first!.id, session!.id) XCTAssertEqual(sessions.first!.state, "foreground") XCTAssertEqual(sessions.first!.endTime!.timeIntervalSince1970, endTime.timeIntervalSince1970, accuracy: 0.001) } @@ -210,7 +177,7 @@ final class SessionControllerTests: XCTestCase { EmbraceHTTPMock.mock(url: testSessionsUrl()) // given a started session - let controller = SessionController(storage: storage, upload: upload) + let controller = SessionController(storage: storage, upload: upload, config: nil) controller.startSession(state: .foreground) // when ending the session @@ -234,7 +201,7 @@ final class SessionControllerTests: XCTestCase { EmbraceHTTPMock.mock(url: testSessionsUrl(), errorCode: 500) // given a started session - let controller = SessionController(storage: storage, upload: upload) + let controller = SessionController(storage: storage, upload: upload, config: nil) controller.startSession(state: .foreground) // when ending the session and the upload fails @@ -266,17 +233,9 @@ final class SessionControllerTests: XCTestCase { XCTAssertEqual(controller.currentSession?.state, "background") } - func test_update_assignsState_toForeground_whenPresent() throws { - controller.startSession(state: .background) - XCTAssertEqual(controller.currentSession?.state, "background") - - controller.update(state: .foreground) - XCTAssertEqual(controller.currentSession?.state, "foreground") - } - func test_update_assignsAppTerminated_toFalse_whenPresent() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = true + session!.appTerminated = true controller.update(appTerminated: false) XCTAssertEqual(controller.currentSession?.appTerminated, false) @@ -284,7 +243,7 @@ final class SessionControllerTests: XCTestCase { func test_update_assignsAppTerminated_toTrue_whenPresent() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(appTerminated: true) XCTAssertEqual(controller.currentSession?.appTerminated, true) @@ -292,39 +251,124 @@ final class SessionControllerTests: XCTestCase { func test_update_changesTo_appTerminated_saveInStorage() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(appTerminated: true) let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") XCTAssertEqual(sessions.first?.appTerminated, true) } func test_update_changesTo_sessionState_saveInStorage() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(state: .background) let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "background") XCTAssertEqual(sessions.first?.appTerminated, false) } + // MARK: background + func test_startup_background_enabled() throws { + + // given background sessions enabled + try mockSuccessfulResponse() + + let configUrlSessionconfig = URLSessionConfiguration.ephemeral + configUrlSessionconfig.httpMaximumConnectionsPerHost = .max + configUrlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] + + let config = EmbraceConfig( + configurable: EditableConfig(isBackgroundSessionEnabled: true), + options: .init(), + notificationCenter: NotificationCenter.default, logger: MockLogger() + ) + wait(delay: .defaultTimeout) + + let controller = SessionController( + storage: storage, + upload: nil, + config: config + ) + + // when starting a cold start session in the background + let session = controller.startSession(state: .background) + + // then the session is created + XCTAssertNotNil(session) + + // when the session ends + controller.endSession() + + // then the session is stored + let sessions: [SessionRecord] = try storage.fetchAll() + XCTAssertEqual(sessions.count, 1) + XCTAssertEqual(sessions.first?.id, session!.id) + XCTAssertEqual(sessions.first?.state, "background") + } + + func mockSuccessfulResponse() throws { + var url = try XCTUnwrap(URL(string: "\(configBaseUrl)/v2/config")) + + if #available(iOS 16.0, *) { + url.append(queryItems: [ + .init(name: "appId", value: TestConstants.appId), + .init(name: "osVersion", value: TestConstants.osVersion), + .init(name: "appVersion", value: TestConstants.appVersion), + .init(name: "sdkVersion", value: TestConstants.sdkVersion) + ]) + } else { + XCTFail("This will fail on versions prior to iOS 16.0") + } + + let path = Bundle.module.path( + forResource: "remote_config_background_enabled", + ofType: "json", + inDirectory: "Mocks" + )! + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + EmbraceHTTPMock.mock(url: url, response: .withData(data, statusCode: 200)) + } + + func test_startup_background_disabled() throws { + + // given background sessions disabled + let controller = SessionController( + storage: storage, + upload: nil, + config: nil + ) + + // when starting a cold start session in the background + let session = controller.startSession(state: .background) + + // then the session is created + XCTAssertNotNil(session) + + // when the session ends + controller.endSession() + + // then the session is not stored + let sessions: [SessionRecord] = try storage.fetchAll() + XCTAssertEqual(sessions.count, 0) + } + // MARK: heartbeat func test_heartbeat() throws { // given a session controller with a 1 second heartbeat invertal - let controller = SessionController(storage: storage, upload: nil, heartbeatInterval: 1) + let controller = SessionController(storage: storage, upload: nil, config: nil, heartbeatInterval: 1) // when starting a session let session = controller.startSession(state: .foreground) - var lastDate = session.lastHeartbeatTime + var lastDate = session!.lastHeartbeatTime // then the heartbeat time is updated every second for _ in 1...3 { @@ -350,4 +394,8 @@ private extension SessionControllerTests { func testLogsUrl(testName: String = #function) -> URL { URL(string: "https://embrace.\(testName).com/session_controller/logs")! } + + private var configBaseUrl: String { + "https://embrace.\(testName).com/config" + } } diff --git a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift index 6545d2ec..7daeed04 100644 --- a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift @@ -6,7 +6,7 @@ import Foundation import XCTest @testable import EmbraceCore import EmbraceCommonInternal -import EmbraceStorageInternal +@testable import EmbraceStorageInternal @testable import EmbraceUploadInternal import TestSupport import GRDB @@ -40,13 +40,9 @@ class UnsentDataHandlerTests: XCTestCase { urlSessionconfig.httpMaximumConnectionsPerHost = .max urlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] - let testCacheOptions = EmbraceUpload.CacheOptions( - cacheBaseUrl: filePathProvider.fileURL(for: testName, name: "upload_cache")! - )! - uploadOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(forTest: testName), - cache: testCacheOptions, + cache: EmbraceUpload.CacheOptions(named: testName), metadata: UnsentDataHandlerTests.testMetadataOptions, redundancy: UnsentDataHandlerTests.testRedundancyOptions, urlSessionConfiguration: urlSessionconfig @@ -337,10 +333,7 @@ class UnsentDataHandlerTests: XCTestCase { startTime: Date(timeIntervalSinceNow: -60) ) - // when sending unsent sessions - UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) - - // then the crash report id and timestamp is set on the session + // the crash report id and timestamp is set on the session let expectation1 = XCTestExpectation() let observation = ValueObservation.tracking(SessionRecord.fetchAll).print() let cancellable = observation.start(in: storage.dbQueue) { error in @@ -352,7 +345,11 @@ class UnsentDataHandlerTests: XCTestCase { } } } - wait(for: [expectation1], timeout: .veryLongTimeout) + + // when sending unsent sessions + UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) + + wait(for: [expectation1], timeout: 5000) cancellable.cancel() // then a crash report was sent @@ -722,6 +719,41 @@ class UnsentDataHandlerTests: XCTestCase { wait(for: [expectation], timeout: .defaultTimeout) } + + func test_logsUpload() throws { + // mock successful requests + EmbraceHTTPMock.mock(url: testSpansUrl()) + EmbraceHTTPMock.mock(url: testLogsUrl()) + + // given a storage and upload modules + let storage = try EmbraceStorage.createInMemoryDb() + defer { try? storage.teardown() } + + let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue) + let logController = LogController(storage: storage, upload: upload, controller: MockSessionController()) + let otel = MockEmbraceOpenTelemetry() + + // given logs in storage + for _ in 0...5 { + try storage.writeLog(LogRecord( + identifier: LogIdentifier.random, + processIdentifier: TestConstants.processId, + severity: .debug, + body: "test", + attributes: [:] + )) + } + + // when sending unsent data + UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, logController: logController) + wait(delay: .longTimeout) + + // then no sessions were sent + XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(testSpansUrl()).count, 0) + + // then a log batch was sent + XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(testLogsUrl()).count, 1) + } } private extension UnsentDataHandlerTests { diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/DummyURLSessionInitWithDelegateSwizzler.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/DummyURLSessionInitWithDelegateSwizzler.swift index ee3a6bba..9fbfb847 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/DummyURLSessionInitWithDelegateSwizzler.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/DummyURLSessionInitWithDelegateSwizzler.swift @@ -30,7 +30,7 @@ class DummyURLSessionInitWithDelegateSwizzler: Swizzlable { class DummyURLProxy: NSObject, URLSessionDelegate { weak var originalDelegate: URLSessionDelegate? - var didForwardToTargetSuccesfully: Bool = false + var didForwardToTargetSuccessfully: Bool = false var didInvokeForwardingTarget: Bool = false var didInvokeRespondsTo: Bool = false var didForwardRespondsToSuccessfullyBool = false @@ -43,10 +43,10 @@ class DummyURLSessionInitWithDelegateSwizzler: Swizzlable { override func responds(to aSelector: Selector!) -> Bool { didInvokeRespondsTo = true if super.responds(to: aSelector) { - didForwardToTargetSuccesfully = true + didForwardToTargetSuccessfully = true return true } else if let originalDelegate = originalDelegate, originalDelegate.responds(to: aSelector) { - didForwardToTargetSuccesfully = true + didForwardToTargetSuccessfully = true return true } return false @@ -55,10 +55,10 @@ class DummyURLSessionInitWithDelegateSwizzler: Swizzlable { override func forwardingTarget(for aSelector: Selector!) -> Any? { didInvokeForwardingTarget = true if super.responds(to: aSelector) { - didForwardToTargetSuccesfully = true + didForwardToTargetSuccessfully = true return self } else if let originalDelegate = originalDelegate, originalDelegate.responds(to: aSelector) { - didForwardToTargetSuccesfully = true + didForwardToTargetSuccessfully = true return originalDelegate } return nil diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift index 5c66adf6..d363bc84 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift @@ -21,12 +21,12 @@ class MockSessionController: SessionControllable { var currentSession: SessionRecord? @discardableResult - func startSession(state: SessionState) -> SessionRecord { + func startSession(state: SessionState) -> SessionRecord? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord { + func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { if currentSession != nil { endSession() } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockUITouch.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockUITouch.swift index 76c0bee4..f5f7d197 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockUITouch.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockUITouch.swift @@ -6,15 +6,15 @@ import UIKit class MockUITouch: UITouch { - private let overridenPhase: Phase + private let overriddenPhase: Phase private let touchedView: UIView init(phase: Phase = .began, touchedView: UIView = .init()) { - self.overridenPhase = phase + self.overriddenPhase = phase self.touchedView = touchedView } - override var phase: UITouch.Phase { overridenPhase } + override var phase: UITouch.Phase { overriddenPhase } override var view: UIView? { touchedView } } #endif diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift deleted file mode 100644 index 75ce9416..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal -import EmbraceCommonInternal - -@testable import EmbraceCore - -class SpySessionController: SessionControllable { - var currentSession: SessionRecord? - - init(currentSession: SessionRecord? = nil) { - self.currentSession = currentSession - } - - func startSession(state: SessionState) -> SessionRecord { - return currentSession! - } - - func endSession() -> Date { Date() } - func update(state: SessionState) {} - func update(appTerminated: Bool) {} -} diff --git a/Tests/EmbraceCrashTests/EmbraceCrashReporterTests.swift b/Tests/EmbraceCrashTests/EmbraceCrashReporterTests.swift index 8b1cc2b1..11005a86 100644 --- a/Tests/EmbraceCrashTests/EmbraceCrashReporterTests.swift +++ b/Tests/EmbraceCrashTests/EmbraceCrashReporterTests.swift @@ -137,7 +137,7 @@ class EmbraceCrashReporterTests: XCTestCase { } func testHavingInternalAddedInfoInKSCrash_appendCrashInfo_shouldntEraseThoseValues() throws { - // given crash reporter with an already setted sdkVersion and sessionId + // given crash reporter with an already set sdkVersion and sessionId let crashReporter = EmbraceCrashReporter() let context = CrashReporterContext( appId: "_-_-_", @@ -149,7 +149,7 @@ class EmbraceCrashReporterTests: XCTestCase { crashReporter.currentSessionId = "original_session_id" let ksCrash = try XCTUnwrap(crashReporter.ksCrash) - // [Intermdiate Assertion to ensure the `given` state] + // [Intermediate Assertion to ensure the `given` state] XCTAssertEqual(ksCrash.userInfo["emb-sid"] as? String, "original_session_id") XCTAssertEqual(ksCrash.userInfo["emb-sdk"] as? String, "1.2.3") diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift index f0c313fb..55105020 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift @@ -48,7 +48,7 @@ extension EmbraceStorageTests { wait(for: [expectation2], timeout: .defaultTimeout) - // the record should update successfuly + // the record should update successfully try storage.dbQueue.read { db in XCTAssert(try span.exists(db)) XCTAssertNotNil(span.endTime) @@ -93,7 +93,7 @@ extension EmbraceStorageTests { wait(for: [expectation2], timeout: .defaultTimeout) - // the record should update successfuly + // the record should update successfully try storage.dbQueue.read { db in XCTAssertFalse(try span.exists(db)) } @@ -137,7 +137,7 @@ extension EmbraceStorageTests { storage.fetchAllAsync { (result: Result<[SpanRecord], Error>) in switch result { case .success(let records): - // then all records should be successfuly fetched + // then all records should be successfully fetched XCTAssert(records.count == 2) XCTAssert(records.contains(span1)) XCTAssert(records.contains(span2)) diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift index 2eaae47c..22f4486a 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift @@ -147,7 +147,7 @@ class EmbraceStorageTests: XCTestCase { try storage.update(record: span) - // the record should update successfuly + // the record should update successfully try storage.dbQueue.read { db in XCTAssert(try span.exists(db)) XCTAssertNotNil(span.endTime) @@ -222,7 +222,7 @@ class EmbraceStorageTests: XCTestCase { // when fetching all records let records: [SpanRecord] = try storage.fetchAll() - // then all records should be successfuly fetched + // then all records should be successfully fetched XCTAssert(records.count == 2) XCTAssert(records.contains(span1)) XCTAssert(records.contains(span2)) diff --git a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift index babb99a4..c6833ed1 100644 --- a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift @@ -515,7 +515,7 @@ class MetadataRecordTests: XCTestCase { try storage.addMetadata(key: "test", value: "test", type: .requiredResource, lifespan: .permanent) // when fetching it - let record = try storage.fetchRequriedPermanentResource(key: "test") + let record = try storage.fetchRequiredPermanentResource(key: "test") // then its correctly fetched XCTAssertNotNil(record) diff --git a/Tests/EmbraceStorageInternalTests/Records/LogRecord/LogRecordTests.swift b/Tests/EmbraceStorageInternalTests/Records/LogRecord/LogRecordTests.swift index 235a90ba..fae9e191 100644 --- a/Tests/EmbraceStorageInternalTests/Records/LogRecord/LogRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/Records/LogRecord/LogRecordTests.swift @@ -21,7 +21,7 @@ class LogRecordTests: XCTestCase { func test_tableSchema() throws { XCTAssertEqual(LogRecord.databaseTableName, "logs") - // then the table and its colums should be correct + // then the table and its columns should be correct try storage.dbQueue.read { db in XCTAssert(try db.tableExists(LogRecord.databaseTableName)) diff --git a/Tests/EmbraceStorageInternalTests/SessionRecordTests.swift b/Tests/EmbraceStorageInternalTests/SessionRecordTests.swift index 043ff20c..571cdc77 100644 --- a/Tests/EmbraceStorageInternalTests/SessionRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/SessionRecordTests.swift @@ -21,7 +21,7 @@ class SessionRecordTests: XCTestCase { func test_tableSchema() throws { XCTAssertEqual(SessionRecord.databaseTableName, "sessions") - // then the table and its colums should be correct + // then the table and its columns should be correct try storage.dbQueue.read { db in XCTAssert(try db.tableExists(SessionRecord.databaseTableName)) @@ -238,13 +238,13 @@ class SessionRecordTests: XCTestCase { ) // when fetching the latest session - let session = try storage.fetchLatestSesssion() + let session = try storage.fetchLatestSession() // then the fetched session is valid XCTAssertEqual(session, session3) } - func test_fetchOldestSesssion() throws { + func test_fetchOldestSession() throws { // given inserted sessions let session1 = try storage.addSession( id: .random, @@ -272,7 +272,7 @@ class SessionRecordTests: XCTestCase { ) // when fetching the oldest session - let session = try storage.fetchOldestSesssion() + let session = try storage.fetchOldestSession() // then the fetched session is valid XCTAssertEqual(session, session1) diff --git a/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift b/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift index aeb6d45e..c3fc5cab 100644 --- a/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift @@ -22,7 +22,7 @@ class SpanRecordTests: XCTestCase { func test_tableSchema() throws { XCTAssertEqual(SpanRecord.databaseTableName, "spans") - // then the table and its colums should be correct + // then the table and its columns should be correct try storage.dbQueue.read { db in XCTAssert(try db.tableExists(SpanRecord.databaseTableName)) @@ -173,7 +173,7 @@ class SpanRecordTests: XCTestCase { } func test_cleanUpSpans() throws { - // given insterted spans + // given inserted spans _ = try storage.addSpan( id: "id1", name: "a name 1", diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift index 4eec8425..607e0e19 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift @@ -8,11 +8,9 @@ import TestSupport extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_date() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting the maximum allowed days - testOptions.cacheDaysLimit = 15 - testOptions.cacheSizeLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 15) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! @@ -94,11 +92,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_date_noLimit() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // disabling maximum allowed days - testOptions.cacheDaysLimit = 0 - testOptions.cacheSizeLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! @@ -165,11 +161,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_date_noRecords() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting minimum allowed time - testOptions.cacheDaysLimit = 1 - testOptions.cacheSizeLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 1) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // when attempting to remove data from an empty cache let removedRecords = try cache.clearStaleDataIfNeeded() @@ -179,11 +173,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_date_didNotHitTimeLimit() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // disabling maximum allowed days - testOptions.cacheDaysLimit = 17 - testOptions.cacheSizeLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 17) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! @@ -255,11 +247,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_size_and_date() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting both limits for days and size - testOptions.cacheDaysLimit = 15 - testOptions.cacheSizeLimit = 1001 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 15, cacheSizeLimit: 1001) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataSize.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataSize.swift index f2007be3..7713d25c 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataSize.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataSize.swift @@ -8,11 +8,9 @@ import TestSupport extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_size() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting the maximum db size of 1000 bytes - testOptions.cacheDaysLimit = 0 - testOptions.cacheSizeLimit = 1000 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheSizeLimit: 1000) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let now = Date() @@ -67,11 +65,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_size_noLimit() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // disabling cache size limit - testOptions.cacheSizeLimit = 0 - testOptions.cacheDaysLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let now = Date() @@ -125,11 +121,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_size_noRecords() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting the maximum db size of 1 byte - testOptions.cacheSizeLimit = 1 - testOptions.cacheDaysLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheSizeLimit: 1) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // when attempting to remove data from an empty cache let removedRecords = try cache.clearStaleDataIfNeeded() @@ -139,11 +133,9 @@ extension EmbraceUploadCacheTests { } func test_clearStaleDataIfNeeded_basedOn_size_didNotHitLimit() throws { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! // setting enough cache limit - testOptions.cacheSizeLimit = 1801 - testOptions.cacheDaysLimit = 0 - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName, cacheSizeLimit: 1801) + let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let now = Date() diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift index 5dbd712d..ada90c8a 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift @@ -8,15 +8,12 @@ import EmbraceOTelInternal @testable import EmbraceUploadInternal class EmbraceUploadCacheTests: XCTestCase { - let testOptions = EmbraceUpload.CacheOptions(cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()))! + let logger = MockLogger() var spanProcessor: MockSpanProcessor! override func setUpWithError() throws { spanProcessor = MockSpanProcessor() EmbraceOTel.setup(spanProcessors: [spanProcessor]) - if FileManager.default.fileExists(atPath: testOptions.cacheFilePath) { - try FileManager.default.removeItem(atPath: testOptions.cacheFilePath) - } } override func tearDownWithError() throws { @@ -25,11 +22,12 @@ class EmbraceUploadCacheTests: XCTestCase { func test_tableSchema() throws { // given new cache - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) let expectation = XCTestExpectation() - // then the table and its colums should be correct + // then the table and its columns should be correct try cache.dbQueue.read { db in XCTAssert(try db.tableExists(UploadDataRecord.databaseTableName)) @@ -89,7 +87,8 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_fetchUploadData() throws { - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data let original = UploadDataRecord( @@ -112,7 +111,8 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_fetchAllUploadData() throws { - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas let data1 = UploadDataRecord(id: "id1", type: 0, data: Data(), attemptCount: 0, date: Date()) @@ -135,7 +135,8 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_saveUploadData() throws { - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data let data = try cache.saveUploadData(id: "id", type: .spans, data: Data()) @@ -152,11 +153,8 @@ class EmbraceUploadCacheTests: XCTestCase { func test_saveUploadData_limit() throws { // given a cache with a limit of 1 - let options = EmbraceUpload.CacheOptions( - cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()), - cacheLimit: 1 - )! - let cache = try EmbraceUploadCache(options: options) + let options = EmbraceUpload.CacheOptions(named: testName, cacheLimit: 1) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas let data1 = try cache.saveUploadData(id: "id1", type: .spans, data: Data()) @@ -177,7 +175,8 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_deleteUploadData() throws { - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data let data = UploadDataRecord( @@ -206,7 +205,8 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_updateAttemptCount() throws { - let cache = try EmbraceUploadCache(options: testOptions) + let options = EmbraceUpload.CacheOptions(named: testName) + let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data let original = UploadDataRecord( diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift index 34c620ca..e7200ed1 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift @@ -8,9 +8,6 @@ import GRDB @testable import EmbraceUploadInternal class EmbraceUploadTests: XCTestCase { - static let testCacheOptions = EmbraceUpload.CacheOptions( - cacheBaseUrl: URL(fileURLWithPath: NSTemporaryDirectory()) - )! static let testMetadataOptions = EmbraceUpload.MetadataOptions( apiKey: "apiKey", userAgent: "userAgent", @@ -23,17 +20,13 @@ class EmbraceUploadTests: XCTestCase { var module: EmbraceUpload! override func setUpWithError() throws { - if FileManager.default.fileExists(atPath: EmbraceUploadTests.testCacheOptions.cacheFilePath) { - try FileManager.default.removeItem(atPath: EmbraceUploadTests.testCacheOptions.cacheFilePath) - } - let urlSessionconfig = URLSessionConfiguration.ephemeral urlSessionconfig.httpMaximumConnectionsPerHost = .max urlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] testOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(testName: testName), - cache: EmbraceUploadTests.testCacheOptions, + cache: EmbraceUpload.CacheOptions(named: testName), metadata: EmbraceUploadTests.testMetadataOptions, redundancy: EmbraceUploadTests.testRedundancyOptions, urlSessionConfiguration: urlSessionconfig diff --git a/Tests/TestSupport/EditableConfig.swift b/Tests/TestSupport/EditableConfig.swift new file mode 100644 index 00000000..1c48e982 --- /dev/null +++ b/Tests/TestSupport/EditableConfig.swift @@ -0,0 +1,41 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import EmbraceConfiguration + +public class EditableConfig: EmbraceConfigurable { + public var isSDKEnabled: Bool = true + + public var isBackgroundSessionEnabled: Bool = false + + public var isNetworkSpansForwardingEnabled: Bool = false + + public var internalLogLimits = InternalLogLimits() + + public var networkPayloadCaptureRules = [NetworkPayloadCaptureRule]() + + public func update(completion: (Bool, (any Error)?) -> Void) { + completion(false, nil) + } + + public init( + isSdkEnabled: Bool = true, + isBackgroundSessionEnabled: Bool = false, + isNetworkSpansForwardingEnabled: Bool = false, + internalLogLimits: InternalLogLimits = InternalLogLimits(), + networkPayloadCaptureRules: [NetworkPayloadCaptureRule] = [] + ) { + self.isSDKEnabled = isSdkEnabled + self.isBackgroundSessionEnabled = isBackgroundSessionEnabled + self.isNetworkSpansForwardingEnabled = false + self.internalLogLimits = internalLogLimits + self.networkPayloadCaptureRules = networkPayloadCaptureRules + } +} + +extension EmbraceConfigurable where Self == DefaultConfig { + public static var editable: EmbraceConfigurable { + return EditableConfig() + } +} diff --git a/Tests/TestSupport/LocalProxy/URLTestProxiedResponse.swift b/Tests/TestSupport/LocalProxy/URLTestProxiedResponse.swift index 541c18ad..c882f147 100644 --- a/Tests/TestSupport/LocalProxy/URLTestProxiedResponse.swift +++ b/Tests/TestSupport/LocalProxy/URLTestProxiedResponse.swift @@ -19,7 +19,7 @@ public class URLTestProxiedResponse { self.error = error } - public static func sucessful( + public static func successful( withData data: Data, response: URLResponse ) -> URLTestProxiedResponse { diff --git a/Tests/TestSupport/Mocks/MockEmbraceConfigurable.swift b/Tests/TestSupport/Mocks/MockEmbraceConfigurable.swift new file mode 100644 index 00000000..13df2f8d --- /dev/null +++ b/Tests/TestSupport/Mocks/MockEmbraceConfigurable.swift @@ -0,0 +1,102 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceConfigInternal +import EmbraceConfiguration +import XCTest + +public class MockEmbraceConfigurable: EmbraceConfigurable { + + public init( + isSDKEnabled: Bool = false, + isBackgroundSessionEnabled: Bool = false, + isNetworkSpansForwardingEnabled: Bool = false, + internalLogLimits: InternalLogLimits = InternalLogLimits(), + networkPayloadCaptureRules: [NetworkPayloadCaptureRule] = [], + updateCompletionParamDidUpdate: Bool = false, + updateCompletionParamError: Error? = nil + ) { + self._isSDKEnabled = isSDKEnabled + self._isBackgroundSessionEnabled = isBackgroundSessionEnabled + self._isNetworkSpansForwardingEnabled = isNetworkSpansForwardingEnabled + self._internalLogLimits = internalLogLimits + self._networkPayloadCaptureRules = networkPayloadCaptureRules + self.updateCompletionParamDidUpdate = updateCompletionParamDidUpdate + self.updateCompletionParamError = updateCompletionParamError + } + + private var _isSDKEnabled: Bool + public let isSDKEnabledExpectation = XCTestExpectation(description: "isSDKEnabled called") + public var isSDKEnabled: Bool { + get { + isSDKEnabledExpectation.fulfill() + return _isSDKEnabled + } + set { + _isSDKEnabled = newValue + } + } + + private var _isBackgroundSessionEnabled: Bool + public let isBackgroundSessionEnabledExpectation = XCTestExpectation( + description: "isBackgroundSessionEnabled called" + ) + public var isBackgroundSessionEnabled: Bool { + get { + isBackgroundSessionEnabledExpectation.fulfill() + return _isBackgroundSessionEnabled + } + set { + _isBackgroundSessionEnabled = newValue + } + } + + private var _isNetworkSpansForwardingEnabled: Bool + public let isNetworkSpansForwardingEnabledExpectation = XCTestExpectation( + description: "isNetworkSpansForwardingEnabled called" ) + public var isNetworkSpansForwardingEnabled: Bool { + get { + isNetworkSpansForwardingEnabledExpectation.fulfill() + return _isNetworkSpansForwardingEnabled + } + set { + _isNetworkSpansForwardingEnabled = newValue + } + } + + private var _internalLogLimits: InternalLogLimits + public let internalLogLimitsExpectation = XCTestExpectation(description: "internalLogLimits called") + public var internalLogLimits: InternalLogLimits { + get { + internalLogLimitsExpectation.fulfill() + return _internalLogLimits + } + set { + _internalLogLimits = newValue + } + } + + private var _networkPayloadCaptureRules: [NetworkPayloadCaptureRule] + public let networkPayloadCaptureRulesExpectation = XCTestExpectation( + description: "networkPayloadCaptureRules called" + ) + public var networkPayloadCaptureRules: [NetworkPayloadCaptureRule] { + get { + networkPayloadCaptureRulesExpectation.fulfill() + return _networkPayloadCaptureRules + } + set { + _networkPayloadCaptureRules = newValue + } + } + + public let updateExpectation = XCTestExpectation(description: "update called") + public var updateCompletionParamDidUpdate: Bool + public var updateCompletionParamError: Error? + public func update(completion: @escaping (Bool, (any Error)?) -> Void) { + updateExpectation.fulfill() + completion(updateCompletionParamDidUpdate, updateCompletionParamError) + } +} diff --git a/Tests/TestSupport/TestConstants.swift b/Tests/TestSupport/TestConstants.swift index 108974fc..8725cd3f 100644 --- a/Tests/TestSupport/TestConstants.swift +++ b/Tests/TestSupport/TestConstants.swift @@ -19,7 +19,7 @@ public struct TestConstants { public static let spanId = "spanId" public static let appId = "appId" - public static let deviceId = "18EDB6CE90C2456B97CB91E0F5941CCA" + public static let deviceId = DeviceIdentifier(string: "18EDB6CE90C2456B97CB91E0F5941CCA")! public static let osVersion = "16.0" public static let sdkVersion = "00.1.00" public static let appVersion = "1.0" diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index e36c03cf..809b1e37 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -21,7 +21,7 @@ { "identity" : "grdb.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift.git", + "location" : "https://github.com/groue/GRDB.swift", "state" : { "revision" : "dd6b98ce04eda39aa22f066cd421c24d7236ea8a", "version" : "6.29.1" @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/embrace-io/KSCrash.git", "state" : { - "revision" : "30df8d35f1cc8d0537d595c4b18ddc0eb2675511", - "version" : "2.0.2" + "revision" : "17ad4c5159145ed550acb04b1cff48e826547265", + "version" : "2.0.4" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 6c90e578..dc6db60d 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -26,7 +26,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/embrace-io/KSCrash.git", - exact: "2.0.2" + exact: "2.0.4" ), .package( url: "https://github.com/open-telemetry/opentelemetry-swift", diff --git a/bin/build b/bin/build index 28c288cb..453ff33d 100755 --- a/bin/build +++ b/bin/build @@ -8,7 +8,7 @@ if [ $# -lt 1 ] ; then echo "Platform list is required as variadic argument:" echo " bin/build [ ...]" echo " " - echo "Valid platforms are: 'iOS', 'macOS', 'tvOS', and 'watchOS'" + echo "Valid platforms are: 'iOS', 'tvOS', and 'watchOS'" exit 1 fi @@ -22,10 +22,10 @@ for INPUT in $@; do # this allows input to be `bin/build ios` or `bin/build iOS` PLATFORM=${PLATFORM/os/OS} - # Validate platform as one of 'iOS', 'macOS', 'watchOS', 'tvOS' - if [ "$PLATFORM" != "iOS" ] && [ "$PLATFORM" != "macOS" ] && [ "$PLATFORM" != "watchOS" ] && [ "$PLATFORM" != "tvOS" ]; then + # Validate platform as one of 'iOS', 'watchOS', 'tvOS' + if [ "$PLATFORM" != "iOS" ] && [ "$PLATFORM" != "watchOS" ] && [ "$PLATFORM" != "tvOS" ]; then echo "Invalid platform '$PLATFORM'" - echo "Must be one of: 'iOS', 'macOS', 'watchOS', 'tvOS'" + echo "Must be one of: 'iOS', 'watchOS', 'tvOS'" exit 1 fi diff --git a/bin/build_xcframeworks.sh b/bin/build_xcframeworks.sh index 13568de5..1888a529 100755 --- a/bin/build_xcframeworks.sh +++ b/bin/build_xcframeworks.sh @@ -74,11 +74,10 @@ create_xcframework EmbraceOTelInternal create_xcframework EmbraceUploadInternal create_xcframework EmbraceConfigInternal +create_xcframework EmbraceConfiguration create_xcframework EmbraceSemantics create_xcframework EmbraceCrash create_xcframework EmbraceCrashlyticsSupport create_xcframework EmbraceCaptureService create_xcframework EmbraceCore create_xcframework EmbraceIO - - diff --git a/bin/dependencies/build_kscrash.sh b/bin/dependencies/build_kscrash.sh index 6166a25b..d04b10bd 100644 --- a/bin/dependencies/build_kscrash.sh +++ b/bin/dependencies/build_kscrash.sh @@ -38,7 +38,7 @@ function create_xcframework { echo "Create $PRODUCT.xcframework" - xcodebuild -create-xcframework -allow-internal-distribution ${xcoptions[@]} -output "$BUILD_DIR/$PRODUCT.xcframework" + xcodebuild -create-xcframework ${xcoptions[@]} -output "$BUILD_DIR/$PRODUCT.xcframework" } mise install @@ -46,9 +46,11 @@ tuist install -p "$REPO_DIR" tuist generate --no-open -p "$REPO_DIR" create_xcframework KSCrashCore -create_xcframework KSCrashFilters -create_xcframework KSCrashSinks -create_xcframework KSCrashInstallations create_xcframework KSCrashRecordingCore -create_xcframework KSCrashReportingCore create_xcframework KSCrashRecording + +# Commented this as they're not necessary to build them right now. +#create_xcframework KSCrashFilters +#create_xcframework KSCrashSinks +#create_xcframework KSCrashInstallations +#create_xcframework KSCrashReportingCore diff --git a/bin/dependencies/build_opentelemetry-swift.sh b/bin/dependencies/build_opentelemetry-swift.sh index 1734935e..693702cc 100644 --- a/bin/dependencies/build_opentelemetry-swift.sh +++ b/bin/dependencies/build_opentelemetry-swift.sh @@ -1,6 +1,16 @@ REPO_DIR=$1 BUILD_DIR=$2 +# We replace the `package` keyword because it's not supported by the `create-xcframework` command. +# We tried using "OTHER_SWIFT_FLAGS" and setting the package and module names, but it ends up creating problems +# since we are building two frameworks at the same time. +# Until the `package` keyword is supported directly, we'll replace the `package` keyword with just `public` +# so the frameworks compile. +# We realize this exposes something that is not meant to be exposed, we think it's harmless. + +grep -rl 'package static' "$REPO_DIR/Sources/" | xargs sed -i '' 's/package static/public static/g' +grep -rl 'package var' "$REPO_DIR/Sources/" | xargs sed -i '' 's/package var/public var/g' + swift create-xcframework --package-path "$REPO_DIR" \ --output "$BUILD_DIR" \ - --platform ios OpenTelemetryApi OpenTelemetrySdk + --platform ios OpenTelemetryApi OpenTelemetrySdk \ No newline at end of file diff --git a/bin/templates/EmbraceIO.podspec.tpl b/bin/templates/EmbraceIO.podspec.tpl index a977eca6..0f3de700 100644 --- a/bin/templates/EmbraceIO.podspec.tpl +++ b/bin/templates/EmbraceIO.podspec.tpl @@ -35,6 +35,7 @@ Pod::Spec.new do |spec| core.dependency "EmbraceIO/EmbraceUploadInternal" core.dependency "EmbraceIO/EmbraceObjCUtilsInternal" core.dependency "EmbraceIO/EmbraceSemantics" + core.dependency "EmbraceIO/EmbraceConfiguration" end spec.subspec 'EmbraceCommonInternal' do |common| @@ -43,6 +44,7 @@ Pod::Spec.new do |spec| spec.subspec 'EmbraceSemantics' do |semantics| semantics.vendored_frameworks = "xcframeworks/EmbraceSemantics.xcframework" + semantics.dependency "EmbraceIO/EmbraceCommonInternal" end spec.subspec 'EmbraceCaptureService' do |capture| @@ -54,6 +56,7 @@ Pod::Spec.new do |spec| spec.subspec 'EmbraceConfigInternal' do |config| config.vendored_frameworks = "xcframeworks/EmbraceConfigInternal.xcframework" config.dependency "EmbraceIO/EmbraceCommonInternal" + config.dependency "EmbraceIO/EmbraceConfiguration" end spec.subspec 'EmbraceOTelInternal' do |otel| @@ -67,7 +70,6 @@ Pod::Spec.new do |spec| storage.vendored_frameworks = "xcframeworks/EmbraceStorageInternal.xcframework" storage.dependency "EmbraceIO/EmbraceCommonInternal" storage.dependency "EmbraceIO/EmbraceSemantics" - storage.dependency "EmbraceIO/OpenTelemetryApi" storage.dependency "EmbraceIO/GRDB" end