diff --git a/examples/swift/hello_world/LoggerCustomer.swift b/examples/swift/hello_world/LoggerCustomer.swift index 22e48e41..16ed099d 100644 --- a/examples/swift/hello_world/LoggerCustomer.swift +++ b/examples/swift/hello_world/LoggerCustomer.swift @@ -90,7 +90,7 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { configuration: .init(), fieldProviders: [CustomFieldProvider()], apiURL: kBitdriftURL - ) + )? .enableIntegrations([.urlSession()], disableSwizzling: true) Logger.addField(withKey: "field_container_field_key", value: "field_container_value") diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt index 7750c9fd..f037e2ad 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt @@ -22,11 +22,33 @@ import okhttp3.HttpUrl import java.util.concurrent.atomic.AtomicReference import kotlin.time.Duration +internal sealed class LoggerState { + /** + * The logger has not yet been configured. + */ + data object NotConfigured : LoggerState() + + /** + * The logger has been successfully configured and is ready for use. Subsequent attempts to configure the logger will be ignored. + */ + class Configured(val logger: LoggerImpl) : LoggerState() + + /** + * The configuration has started but is not yet complete. Subsequent attempts to configure the logger will be ignored. + */ + data object ConfigurationStarted : LoggerState() + + /** + * The configuration was attempted but failed. Subsequent attempts to configure the logger will be ignored. + */ + data object ConfigurationFailure : LoggerState() +} + /** * Top level namespace Capture SDK. */ object Capture { - private val default: AtomicReference = AtomicReference(null) + private val default: AtomicReference = AtomicReference(LoggerState.NotConfigured) /** * Returns a handle to the underlying logger instance, if Capture has been configured. @@ -34,7 +56,12 @@ object Capture { * @return ILogger a logger handle */ fun logger(): ILogger? { - return default.get() + return when (val state = default.get()) { + is LoggerState.NotConfigured -> null + is LoggerState.Configured -> state.logger + is LoggerState.ConfigurationStarted -> null + is LoggerState.ConfigurationFailure -> null + } } /** @@ -82,6 +109,29 @@ object Capture { fieldProviders: List = listOf(), dateProvider: DateProvider? = null, apiUrl: HttpUrl = defaultCaptureApiUrl, + ) { + configure( + apiKey, + sessionStrategy, + configuration, + fieldProviders, + dateProvider, + apiUrl, + CaptureJniLibrary, + ) + } + + @Synchronized + @JvmStatic + @JvmOverloads + internal fun configure( + apiKey: String, + sessionStrategy: SessionStrategy, + configuration: Configuration = Configuration(), + fieldProviders: List = listOf(), + dateProvider: DateProvider? = null, + apiUrl: HttpUrl = defaultCaptureApiUrl, + bridge: IBridge, ) { // Note that we need to use @Synchronized to prevent multiple loggers from being initialized, // while subsequent logger access relies on volatile reads. @@ -96,22 +146,26 @@ object Capture { return } - // If the logger has already been configured, do nothing. - if (default.get() != null) { + // Ideally we would use `getAndUpdate` in here but it's available for API 24 and up only. + if (default.compareAndSet(LoggerState.NotConfigured, LoggerState.ConfigurationStarted)) { + try { + val logger = LoggerImpl( + apiKey = apiKey, + apiUrl = apiUrl, + fieldProviders = fieldProviders, + dateProvider = dateProvider ?: SystemDateProvider(), + configuration = configuration, + sessionStrategy = sessionStrategy, + bridge = bridge, + ) + default.set(LoggerState.Configured(logger)) + } catch (e: Throwable) { + Log.w("capture", "Capture initialization failed", e) + default.set(LoggerState.ConfigurationFailure) + } + } else { Log.w("capture", "Attempted to initialize Capture more than once") - return } - - val logger = LoggerImpl( - apiKey = apiKey, - apiUrl = apiUrl, - fieldProviders = fieldProviders, - dateProvider = dateProvider ?: SystemDateProvider(), - configuration = configuration, - sessionStrategy = sessionStrategy, - ) - - default.set(logger) } /** @@ -333,7 +387,7 @@ object Capture { */ @JvmStatic fun log(httpRequestInfo: HttpRequestInfo) { - default.get()?.log(httpRequestInfo) + logger()?.log(httpRequestInfo) } /** @@ -344,7 +398,14 @@ object Capture { */ @JvmStatic fun log(httpResponseInfo: HttpResponseInfo) { - default.get()?.log(httpResponseInfo) + logger()?.log(httpResponseInfo) + } + + /** + * Used for testing purposes. + */ + internal fun resetShared() { + default.set(LoggerState.NotConfigured) } } } diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt index 6e51bf73..2f363e67 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt @@ -25,7 +25,7 @@ interface StackTraceProvider { } @Suppress("UndocumentedPublicClass") -internal object CaptureJniLibrary { +internal object CaptureJniLibrary : IBridge { /** * Loads the shared library. This is safe to call multiple times. @@ -49,7 +49,7 @@ internal object CaptureJniLibrary { * @param preferences the preferences storage to use for persistent storage of simple settings and configuration. * @param errorReporter the error reporter to use for reporting error to bitdrift services. */ - external fun createLogger( + external override fun createLogger( sdkDirectory: String, apiKey: String, sessionStrategy: SessionStrategyConfiguration, diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt new file mode 100644 index 00000000..89a04c63 --- /dev/null +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IBridge.kt @@ -0,0 +1,28 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import io.bitdrift.capture.error.IErrorReporter +import io.bitdrift.capture.network.ICaptureNetwork +import io.bitdrift.capture.providers.session.SessionStrategyConfiguration + +internal interface IBridge { + fun createLogger( + sdkDirectory: String, + apiKey: String, + sessionStrategy: SessionStrategyConfiguration, + metadataProvider: IMetadataProvider, + resourceUtilizationTarget: IResourceUtilizationTarget, + eventsListenerTarget: IEventsListenerTarget, + applicationId: String, + applicationVersion: String, + network: ICaptureNetwork, + preferences: IPreferences, + errorReporter: IErrorReporter, + ): Long +} diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt index 6c34120b..4a85687d 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt @@ -76,6 +76,7 @@ internal class LoggerImpl( private val apiClient: OkHttpApiClient = OkHttpApiClient(apiUrl, apiKey), private var deviceCodeService: DeviceCodeService = DeviceCodeService(apiClient), private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager, + private val bridge: IBridge = CaptureJniLibrary, ) : ILogger { private val metadataProvider: MetadataProvider @@ -144,7 +145,7 @@ internal class LoggerImpl( processingQueue, ) - this.loggerId = CaptureJniLibrary.createLogger( + val loggerId = bridge.createLogger( sdkDirectory, apiKey, sessionStrategy.createSessionStrategyConfiguration { appExitSaveCurrentSessionId(it) }, @@ -164,6 +165,10 @@ internal class LoggerImpl( localErrorReporter, ) + check(loggerId != -1L) { "initialization of the rust logger failed" } + + this.loggerId = loggerId + runtime = JniRuntime(this.loggerId) diskUsageMonitor.runtime = runtime diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTest.kt index 1d55bc52..0b7f141a 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureTest.kt @@ -26,11 +26,10 @@ import org.robolectric.annotation.Config @Config(sdk = [21]) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class CaptureTest { - // This Test needs to run first since the following tests need to initialize // the ContextHolder before they can run. @Test - fun a_configure_skips_logger_creation_when_context_not_initialized() { + fun aConfigureSkipsLoggerCreationWhenContextNotInitialized() { assertThat(Capture.logger()).isNull() Logger.configure( @@ -45,7 +44,7 @@ class CaptureTest { // Accessing fields prior to the configuration of the logger may lead to crash since it can // potentially call into a native method that's used to sanitize passed url path. @Test - fun b_does_not_access_fields_if_logger_not_configured() { + fun bDoesNotAccessFieldsIfLoggerNotConfigured() { assertThat(Capture.logger()).isNull() val requestInfo = HttpRequestInfo("GET", path = HttpUrlPath("/foo/12345")) @@ -65,7 +64,7 @@ class CaptureTest { } @Test - fun c_idempotent_configure() { + fun cIdempotentConfigure() { val initializer = ContextHolder() initializer.create(ApplicationProvider.getApplicationContext()) diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt new file mode 100644 index 00000000..a95d14dd --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt @@ -0,0 +1,108 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.bitdrift.capture.providers.session.SessionStrategy +import org.assertj.core.api.Assertions +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21]) +class ConfigurationTest { + @Test + fun configurationFailure() { + val initializer = ContextHolder() + initializer.create(ApplicationProvider.getApplicationContext()) + + val bridge: IBridge = mock {} + whenever( + bridge.createLogger( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ), + ).thenReturn(-1L) + + // We start without configured logger. + Assertions.assertThat(Capture.logger()).isNull() + + Capture.Logger.configure( + apiKey = "test1", + sessionStrategy = SessionStrategy.Fixed(), + dateProvider = null, + bridge = bridge, + ) + + // The configuration failed so the logger is still `null`. + Assertions.assertThat(Capture.logger()).isNull() + + // We confirm that we actually tried to configure the logger. + verify(bridge, times(1)).createLogger( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + + // We perform another attempt to configure the logger to verify that + // consecutive configure calls are no-ops. + Capture.Logger.configure( + apiKey = "test1", + sessionStrategy = SessionStrategy.Fixed(), + dateProvider = null, + bridge = bridge, + ) + + Assertions.assertThat(Capture.logger()).isNull() + + // We verify that the second configure call was a no-op. + verify(bridge, times(1)).createLogger( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @After + fun tearDown() { + Capture.Logger.resetShared() + } +} diff --git a/platform/swift/source/Capture.swift b/platform/swift/source/Capture.swift index f2fe5b55..6ffaeea1 100644 --- a/platform/swift/source/Capture.swift +++ b/platform/swift/source/Capture.swift @@ -19,8 +19,9 @@ extension Logger { // MARK: - General - /// An instance of an underlying logger, if the Capture SDK has been configured. Returns `nil` only if - /// `configure(...)` method has not been called. + /// An instance of the underlying logger, if the Capture SDK is configured. Returns `nil` if the + /// `configure(...)` method has not been called or if Logger configuration failed due to an internal + /// error. public static var shared: Logging? { self.getShared(assert: false) } @@ -40,8 +41,7 @@ extension Logger { /// instructed otherwise during discussions with bitdrift. Defaults to /// bitdrift's hosted Compose API base URL. /// - /// - returns: A logger integrator that can be used to enable various SDK integration and get access - /// to non-optional `Logging` instance. + /// - returns: A logger integrator that can be used to enable various SDK integration. @discardableResult public static func configure( withAPIKey apiKey: String, @@ -51,7 +51,29 @@ extension Logger { dateProvider: DateProvider? = nil, // swiftlint:disable:next force_unwrapping use_static_string_url_init apiURL: URL = URL(string: "https://api.bitdrift.io")! - ) -> LoggerIntegrator + ) -> LoggerIntegrator? + { + return self.configure( + withAPIKey: apiKey, + sessionStrategy: sessionStrategy, + configuration: configuration, + fieldProviders: fieldProviders, + dateProvider: dateProvider, + apiURL: apiURL, + loggerBridgingFactoryProvider: LoggerBridgingFactory() + ) + } + + @discardableResult + static func configure( + withAPIKey apiKey: String, + sessionStrategy: SessionStrategy, + configuration: Configuration, + fieldProviders: [FieldProvider], + dateProvider: DateProvider?, + apiURL: URL, + loggerBridgingFactoryProvider: LoggerBridgingFactoryProvider + ) -> LoggerIntegrator? { return self.createOnce { return Logger( @@ -60,7 +82,8 @@ extension Logger { configuration: configuration, sessionStrategy: sessionStrategy, dateProvider: dateProvider, - fieldProviders: fieldProviders + fieldProviders: fieldProviders, + loggerBridgingFactoryProvider: loggerBridgingFactoryProvider ) } } diff --git a/platform/swift/source/Logger.swift b/platform/swift/source/Logger.swift index ab0328fb..3e0c5f12 100644 --- a/platform/swift/source/Logger.swift +++ b/platform/swift/source/Logger.swift @@ -11,6 +11,17 @@ import Foundation // swiftlint:disable file_length public final class Logger { + enum State { + // The logger has not yet been configured. + case notConfigured + // The logger has been successfully configured and is ready for use. + // Subsequent attempts to configure the logger will be ignored. + case configured(LoggerIntegrator) + // The configuration was attempted but failed. + // Subsequent attempts to configure the logger will be ignored. + case configurationFailure + } + private let underlyingLogger: CoreLogging private let timeProvider: TimeProvider @@ -24,7 +35,7 @@ public final class Logger { private let sessionURLBase: URL - private static let syncedShared = Atomic(nil) + private static let syncedShared = Atomic(.notConfigured) private let network: URLSessionNetworkClient? // Used for benchmarking purposes. @@ -46,7 +57,7 @@ public final class Logger { /// attributes to attach to emitted logs. /// - parameter loggerBridgingFactoryProvider: A class to use for Rust bridging. Used for testing /// purposes. - convenience init( + convenience init?( withAPIKey apiKey: String, apiURL: URL, configuration: Configuration, @@ -94,7 +105,7 @@ public final class Logger { /// - parameter timeProvider: The time source to use by the logger. /// - parameter loggerBridgingFactoryProvider: A class to use for Rust bridging. Used for testing /// purposes. - init( + init?( withAPIKey apiKey: String, bufferDirectory: URL?, apiURL: URL, @@ -111,10 +122,6 @@ public final class Logger { { self.timeProvider = timeProvider let start = timeProvider.uptime() - defer { - let duration = timeProvider.timeIntervalSince(start) - self.underlyingLogger.logSDKConfigured(fields: [:], duration: duration) - } // Order of providers matters in here, the latter in the list the higher their priority in // case of key conflicts. @@ -157,26 +164,33 @@ public final class Logger { ) self.eventsListenerTarget = EventsListenerTarget() - self.underlyingLogger = CoreLogger( - logger: loggerBridgingFactoryProvider.makeLogger( - apiKey: apiKey, - bufferDirectoryPath: directoryURL?.path, - sessionStrategy: sessionStrategy, - metadataProvider: metadataProvider, - // TODO(Augustyniak): Pass `resourceUtilizationTarget` and `eventsListenerTarget` as part of - // the `self.underlyingLogger.start()` method call instead. - // Pass the event listener target here and finish setting up - // before the logger is actually started. - resourceUtilizationTarget: self.resourceUtilizationTarget, - // Pass the event listener target here and finish setting up - // before the logger is actually started. - eventsListenerTarget: self.eventsListenerTarget, - appID: clientAttributes.appID, - releaseVersion: clientAttributes.appVersion, - network: network, - errorReporting: self.remoteErrorReporter - ) - ) + guard let logger = loggerBridgingFactoryProvider.makeLogger( + apiKey: apiKey, + bufferDirectoryPath: directoryURL?.path, + sessionStrategy: sessionStrategy, + metadataProvider: metadataProvider, + // TODO(Augustyniak): Pass `resourceUtilizationTarget` and `eventsListenerTarget` as part of + // the `self.underlyingLogger.start()` method call instead. + // Pass the event listener target here and finish setting up + // before the logger is actually started. + resourceUtilizationTarget: self.resourceUtilizationTarget, + // Pass the event listener target here and finish setting up + // before the logger is actually started. + eventsListenerTarget: self.eventsListenerTarget, + appID: clientAttributes.appID, + releaseVersion: clientAttributes.appVersion, + network: network, + errorReporting: self.remoteErrorReporter + ) else { + return nil + } + + self.underlyingLogger = CoreLogger(logger: logger) + + defer { + let duration = timeProvider.timeIntervalSince(start) + self.underlyingLogger.logSDKConfigured(fields: [:], duration: duration) + } self.eventsListenerTarget.setUp( logger: self.underlyingLogger, @@ -247,19 +261,25 @@ public final class Logger { // MARK: - Static - static func createOnce(_ createLogger: () -> Logger) -> LoggerIntegrator { - let logger = self.syncedShared.update { logger in - guard logger == nil else { + static func createOnce(_ createLogger: () -> Logger?) -> LoggerIntegrator? { + let state = self.syncedShared.update { state in + guard case .notConfigured = state else { return } - logger = LoggerIntegrator(logger: createLogger()) + if let createdLogger = createLogger() { + state = .configured(LoggerIntegrator(logger: createdLogger)) + } else { + state = .configurationFailure + } } - // Safety: - // The logger instance is guaranteed to be created after the first call to createOnce method. - // swiftlint:disable force_unwrapping - return logger! + return switch state { + case .configured(let logger): + logger + case .notConfigured, .configurationFailure: + nil + } } /// Retrieves a shared instance of logger if one has been configured. @@ -268,7 +288,8 @@ public final class Logger { /// /// - returns: The shared instance of logger. static func getShared(assert: Bool = true) -> Logging? { - guard let integrator = Self.syncedShared.load() else { + return switch Self.syncedShared.load() { + case .notConfigured: { if assert { assertionFailure( """ @@ -279,16 +300,25 @@ public final class Logger { } return nil + }() + case .configured(let integrator): + integrator.logger + case .configurationFailure: + nil } - - return integrator.logger } /// Internal for testing purposes only. /// /// - parameter logger: The logger to use. static func resetShared(logger: Logging? = nil) { - Self.syncedShared.update { $0 = logger.flatMap(LoggerIntegrator.init(logger:)) } + Self.syncedShared.update { state in + if let logger { + state = .configured(LoggerIntegrator(logger: logger)) + } else { + state = .notConfigured + } + } } // Returns the location to use for storing files related to the Capture SDK, including the disk persisted diff --git a/platform/swift/source/LoggerBridge.swift b/platform/swift/source/LoggerBridge.swift index 41d68166..820b7a7e 100644 --- a/platform/swift/source/LoggerBridge.swift +++ b/platform/swift/source/LoggerBridge.swift @@ -16,7 +16,7 @@ final class LoggerBridge: LoggerBridging { let loggerID: LoggerID private var blockingShutdown = false - init( + init?( apiKey: String, bufferDirectoryPath: String?, sessionStrategy: SessionStrategy, @@ -28,7 +28,7 @@ final class LoggerBridge: LoggerBridging { network: Network?, errorReporting: RemoteErrorReporting ) { - self.loggerID = capture_create_logger( + let loggerID = capture_create_logger( bufferDirectoryPath, apiKey, sessionStrategy.makeSessionStrategyProvider(), @@ -40,6 +40,12 @@ final class LoggerBridge: LoggerBridging { network, errorReporting ) + + if loggerID == -1 { + return nil + } + + self.loggerID = loggerID } deinit { @@ -61,7 +67,7 @@ final class LoggerBridge: LoggerBridging { releaseVersion: String, network: Network?, errorReporting: RemoteErrorReporting - ) -> LoggerBridging { + ) -> LoggerBridging? { return LoggerBridge( apiKey: apiKey, bufferDirectoryPath: bufferDirectoryPath, diff --git a/platform/swift/source/LoggerBridgingFactory.swift b/platform/swift/source/LoggerBridgingFactory.swift index 5c8f8f5a..ed55498e 100644 --- a/platform/swift/source/LoggerBridgingFactory.swift +++ b/platform/swift/source/LoggerBridgingFactory.swift @@ -19,7 +19,7 @@ final class LoggerBridgingFactory: LoggerBridgingFactoryProvider { releaseVersion: String, network: Network?, errorReporting: RemoteErrorReporting - ) -> LoggerBridging { + ) -> LoggerBridging? { return LoggerBridge( apiKey: apiKey, bufferDirectoryPath: bufferDirectoryPath, diff --git a/platform/swift/source/LoggerBridgingFactoryProvider.swift b/platform/swift/source/LoggerBridgingFactoryProvider.swift index cd57d0d8..15a1d33b 100644 --- a/platform/swift/source/LoggerBridgingFactoryProvider.swift +++ b/platform/swift/source/LoggerBridgingFactoryProvider.swift @@ -34,5 +34,5 @@ protocol LoggerBridgingFactoryProvider { releaseVersion: String, network: Network?, errorReporting: RemoteErrorReporting - ) -> LoggerBridging + ) -> LoggerBridging? } diff --git a/test/platform/swift/benchmark/ClockTimeProfiler.swift b/test/platform/swift/benchmark/ClockTimeProfiler.swift index b639f913..7eac0cf6 100644 --- a/test/platform/swift/benchmark/ClockTimeProfiler.swift +++ b/test/platform/swift/benchmark/ClockTimeProfiler.swift @@ -10,6 +10,7 @@ import Benchmark import CapturePassable import CaptureTestBridge import Foundation +import XCTest // swiftlint:disable:next force_unwrapping private let kAPIURL = URL(string: "https://api-tests.bitdrift.io")! @@ -276,7 +277,7 @@ private let kPostConfigLogBenchmark = BenchmarkSuite(name: "Logging - Post-Confi private extension Logger { static func make(directoryURL: URL? = nil) throws -> Logger { let directoryURLFallback = try makeTmpDirectory() - return Logger( + return try XCTUnwrap(Logger( withAPIKey: "foo", bufferDirectory: directoryURL ?? directoryURLFallback, apiURL: kAPIURL, @@ -289,6 +290,7 @@ private extension Logger { storageProvider: Storage.shared, timeProvider: SystemTimeProvider() ) + ) } } diff --git a/test/platform/swift/unit_integration/ConfigurationTests.swift b/test/platform/swift/unit_integration/ConfigurationTests.swift index 9aeca81a..3b883035 100644 --- a/test/platform/swift/unit_integration/ConfigurationTests.swift +++ b/test/platform/swift/unit_integration/ConfigurationTests.swift @@ -42,4 +42,34 @@ final class ConfigurationTests: XCTestCase { XCTAssertNotNil(Logger.getShared()) } + + func testConfigurationFailure() { + let factory = MockLoggerBridgingFactory(logger: nil) + + Logger.configure( + withAPIKey: "test", + sessionStrategy: .fixed(), + configuration: .init(), + fieldProviders: [], + dateProvider: nil, + apiURL: URL(staticString: "https://api.bitdrift.io"), + loggerBridgingFactoryProvider: factory + ) + + XCTAssertEqual(1, factory.makeLoggerCallsCount) + XCTAssertNil(Logger.shared) + + Logger.configure( + withAPIKey: "test", + sessionStrategy: .fixed(), + configuration: .init(), + fieldProviders: [], + dateProvider: nil, + apiURL: URL(staticString: "https://api.bitdrift.io"), + loggerBridgingFactoryProvider: factory + ) + + XCTAssertEqual(1, factory.makeLoggerCallsCount) + XCTAssertNil(Logger.shared) + } } diff --git a/test/platform/swift/unit_integration/LoggerSharedTests.swift b/test/platform/swift/unit_integration/LoggerSharedTests.swift index b9f44119..2d5e03b2 100644 --- a/test/platform/swift/unit_integration/LoggerSharedTests.swift +++ b/test/platform/swift/unit_integration/LoggerSharedTests.swift @@ -19,15 +19,17 @@ final class LoggerSharedTests: XCTestCase { Logger.resetShared() } - func testIntegrationsAreEnabledOnlyOnce() { + func testIntegrationsAreEnabledOnlyOnce() throws { var integrationStartsCount = 0 let integration = Integration { _, _ in integrationStartsCount += 1 } - let integrator = Logger.configure( - withAPIKey: "foo", - sessionStrategy: .fixed() + let integrator = try XCTUnwrap( + Logger.configure( + withAPIKey: "foo", + sessionStrategy: .fixed() + ) ) integrator.enableIntegrations([integration]) diff --git a/test/platform/swift/unit_integration/LoggerTests.swift b/test/platform/swift/unit_integration/LoggerTests.swift index 4f94272f..a41c70a1 100644 --- a/test/platform/swift/unit_integration/LoggerTests.swift +++ b/test/platform/swift/unit_integration/LoggerTests.swift @@ -11,8 +11,8 @@ import Foundation import XCTest final class LoggerTests: XCTestCase { - func testPropertiesReturnsCorrectValues() { - let logger = Logger.testLogger( + func testPropertiesReturnsCorrectValues() throws { + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(sessionReplayConfiguration: nil) @@ -23,8 +23,8 @@ final class LoggerTests: XCTestCase { } // Basic test to ensure we can create a logger and call log. - func testLogger() { - let logger = Logger.testLogger( + func testLogger() throws { + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(sessionReplayConfiguration: nil) @@ -37,7 +37,7 @@ final class LoggerTests: XCTestCase { // Verifies that we don't end up recursing forever (resulting in a stack overflow) when a provider ends // up calling back into the logger. - func testPreventsLoggingReEntryFromWithinRegisteredProviders() { + func testPreventsLoggingReEntryFromWithinRegisteredProviders() throws { var logger: Logger? let expectation = self.expectation(description: "'SDKConfigured' log is emitted") @@ -49,7 +49,7 @@ final class LoggerTests: XCTestCase { return [:] } - logger = Logger.testLogger( + logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), fieldProviders: [fieldProvider], @@ -59,7 +59,7 @@ final class LoggerTests: XCTestCase { XCTAssertEqual(.completed, XCTWaiter().wait(for: [expectation], timeout: 1)) } - func testRunsProvidersOffCallerThread() { + func testRunsProvidersOffCallerThread() throws { let dateProvider = MockDateProvider() // Verify that test is executed on the main queue. @@ -93,7 +93,7 @@ final class LoggerTests: XCTestCase { return Date() } - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), sessionStrategy: SessionStrategy.fixed(), @@ -112,7 +112,7 @@ final class LoggerTests: XCTestCase { XCTAssertEqual(.completed, XCTWaiter().wait(for: expectations, timeout: 1)) } - func testFailingEncodableField() { + func testFailingEncodableField() throws { struct FailingEncodable: Encodable { struct Error: Swift.Error {} @@ -123,7 +123,7 @@ final class LoggerTests: XCTestCase { let bridge = MockLoggerBridging() - let logger = Logger.testLogger( + let logger = try Logger.testLogger( bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(), loggerBridgingFactoryProvider: MockLoggerBridgingFactory(logger: bridge) @@ -144,7 +144,7 @@ final class LoggerTests: XCTestCase { let bridge = MockLoggerBridging() - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), @@ -192,7 +192,7 @@ final class LoggerTests: XCTestCase { let bridge = MockLoggerBridging() - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), @@ -242,7 +242,7 @@ final class LoggerTests: XCTestCase { let bridge = MockLoggerBridging() - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), configuration: .init(sessionReplayConfiguration: .init(captureIntervalSeconds: 5)), diff --git a/test/platform/swift/unit_integration/SessionStrategyTests.swift b/test/platform/swift/unit_integration/SessionStrategyTests.swift index 0cc43ac5..d0a1a941 100644 --- a/test/platform/swift/unit_integration/SessionStrategyTests.swift +++ b/test/platform/swift/unit_integration/SessionStrategyTests.swift @@ -15,10 +15,10 @@ final class SessionStrategyTests: XCTestCase { Storage.shared.clear() } - func testFixedSessionStrategy() { + func testFixedSessionStrategy() throws { var generatedSessionIDs = [String]() - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), sessionStrategy: SessionStrategy.fixed { @@ -40,11 +40,11 @@ final class SessionStrategyTests: XCTestCase { XCTAssertEqual(logger.sessionID, generatedSessionIDs[1]) } - func testActivityBasedSessionStrategy() { + func testActivityBasedSessionStrategy() throws { let expectation = self.expectation(description: "onSessionIDChange called") var observedSessionID: String? - let logger = Logger.testLogger( + let logger = try Logger.testLogger( withAPIKey: "test_api_key", bufferDirectory: Logger.tempBufferDirectory(), sessionStrategy: SessionStrategy.activityBased { sessionID in diff --git a/test/platform/swift/unit_integration/integrations/URLSessionIntegrationTests.swift b/test/platform/swift/unit_integration/integrations/URLSessionIntegrationTests.swift index 9d91162e..627222d6 100644 --- a/test/platform/swift/unit_integration/integrations/URLSessionIntegrationTests.swift +++ b/test/platform/swift/unit_integration/integrations/URLSessionIntegrationTests.swift @@ -56,7 +56,7 @@ final class URLSessionIntegrationTests: XCTestCase { self.logger = MockLogging() Logger.resetShared(logger: self.logger) Logger - .configure(withAPIKey: "123", sessionStrategy: .fixed()) + .configure(withAPIKey: "123", sessionStrategy: .fixed())? .enableIntegrations([ .urlSession(), ]) diff --git a/test/platform/swift/unit_integration/mocks/Logger+Tests.swift b/test/platform/swift/unit_integration/mocks/Logger+Tests.swift index 970e6c1b..fbaa9647 100644 --- a/test/platform/swift/unit_integration/mocks/Logger+Tests.swift +++ b/test/platform/swift/unit_integration/mocks/Logger+Tests.swift @@ -7,6 +7,7 @@ @testable import Capture import Foundation +import XCTest extension Logger { static func testLogger( @@ -17,20 +18,22 @@ extension Logger { fieldProviders: [FieldProvider] = [], configuration: Configuration, loggerBridgingFactoryProvider: LoggerBridgingFactoryProvider = LoggerBridgingFactory() - ) -> Logger + ) throws -> Logger { - return Logger( - withAPIKey: apiKey, - bufferDirectory: bufferDirectory, - apiURL: URL(staticString: "https://api-tests.bitdrift.io"), - remoteErrorReporter: nil, - configuration: configuration, - sessionStrategy: sessionStrategy, - dateProvider: dateProvider, - fieldProviders: fieldProviders, - storageProvider: MockStorageProvider(), - timeProvider: SystemTimeProvider(), - loggerBridgingFactoryProvider: loggerBridgingFactoryProvider + return try XCTUnwrap( + Logger( + withAPIKey: apiKey, + bufferDirectory: bufferDirectory, + apiURL: URL(staticString: "https://api-tests.bitdrift.io"), + remoteErrorReporter: nil, + configuration: configuration, + sessionStrategy: sessionStrategy, + dateProvider: dateProvider, + fieldProviders: fieldProviders, + storageProvider: MockStorageProvider(), + timeProvider: SystemTimeProvider(), + loggerBridgingFactoryProvider: loggerBridgingFactoryProvider + ) ) } diff --git a/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift b/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift index c5f8cf74..9ab04f17 100644 --- a/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift +++ b/test/platform/swift/unit_integration/mocks/MockLoggerBridgingFactory.swift @@ -10,9 +10,10 @@ import CaptureLoggerBridge import Foundation final class MockLoggerBridgingFactory: LoggerBridgingFactoryProvider { - private let logger: LoggerBridging + private let logger: LoggerBridging? + private(set) var makeLoggerCallsCount = 0 - init(logger: LoggerBridging) { + init(logger: LoggerBridging?) { self.logger = logger } @@ -27,7 +28,8 @@ final class MockLoggerBridgingFactory: LoggerBridgingFactoryProvider { releaseVersion _: String, network _: Network?, errorReporting _: RemoteErrorReporting - ) -> LoggerBridging { + ) -> LoggerBridging? { + self.makeLoggerCallsCount += 1 return self.logger } } diff --git a/test/platform/swift/unit_integration/network/LoggerE2ETest.swift b/test/platform/swift/unit_integration/network/LoggerE2ETest.swift index 60bbcb32..fa2c5d7d 100644 --- a/test/platform/swift/unit_integration/network/LoggerE2ETest.swift +++ b/test/platform/swift/unit_integration/network/LoggerE2ETest.swift @@ -64,26 +64,28 @@ final class CaptureE2ENetworkTests: BaseNetworkingTestCase { self.storage = MockStorageProvider() let apiURL = self.setUpTestServer() - let logger = Logger( - withAPIKey: "test!", - bufferDirectory: self.setUpSDKDirectory(), - apiURL: apiURL, - remoteErrorReporter: MockRemoteErrorReporter(), - configuration: configuration, - sessionStrategy: SessionStrategy.fixed(sessionIDGenerator: { "mock-group-id" }), - dateProvider: MockDateProvider(), - fieldProviders: [ - MockFieldProvider( - getFieldsClosure: { - [ - "field_provider": "mock_field_provider", - "failing_to_convert_and_should_be_skipped_field": MockEncodable(), - ] - } - ), - ], - storageProvider: self.storage, - timeProvider: SystemTimeProvider() + let logger = try XCTUnwrap( + Logger( + withAPIKey: "test!", + bufferDirectory: self.setUpSDKDirectory(), + apiURL: apiURL, + remoteErrorReporter: MockRemoteErrorReporter(), + configuration: configuration, + sessionStrategy: SessionStrategy.fixed(sessionIDGenerator: { "mock-group-id" }), + dateProvider: MockDateProvider(), + fieldProviders: [ + MockFieldProvider( + getFieldsClosure: { + [ + "field_provider": "mock_field_provider", + "failing_to_convert_and_should_be_skipped_field": MockEncodable(), + ] + } + ), + ], + storageProvider: self.storage, + timeProvider: SystemTimeProvider() + ) ) self.logger = logger diff --git a/test/platform/swift/unit_integration/network/helper/BaseNetworkingTestCase.swift b/test/platform/swift/unit_integration/network/helper/BaseNetworkingTestCase.swift index 687b3a34..05348fcc 100644 --- a/test/platform/swift/unit_integration/network/helper/BaseNetworkingTestCase.swift +++ b/test/platform/swift/unit_integration/network/helper/BaseNetworkingTestCase.swift @@ -68,17 +68,19 @@ open class BaseNetworkingTestCase: XCTestCase { self.network = network - let loggerBridge = LoggerBridge( - apiKey: "test!", - bufferDirectoryPath: sdkDirectory.path, - sessionStrategy: .fixed(), - metadataProvider: MockMetadataProvider(), - resourceUtilizationTarget: MockResourceUtilizationTarget(), - eventsListenerTarget: MockEventsListenerTarget(), - appID: "io.bitdrift.capture.test", - releaseVersion: "", - network: network, - errorReporting: MockRemoteErrorReporter() + let loggerBridge = try XCTUnwrap( + LoggerBridge( + apiKey: "test!", + bufferDirectoryPath: sdkDirectory.path, + sessionStrategy: .fixed(), + metadataProvider: MockMetadataProvider(), + resourceUtilizationTarget: MockResourceUtilizationTarget(), + eventsListenerTarget: MockEventsListenerTarget(), + appID: "io.bitdrift.capture.test", + releaseVersion: "", + network: network, + errorReporting: MockRemoteErrorReporter() + ) ) loggerBridge.start()