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 124df9ac9..223393d53 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 @@ -10,6 +10,7 @@ package io.bitdrift.capture import android.content.Context import android.util.Log import com.github.michaelbull.result.Err +import io.bitdrift.capture.common.IBackgroundThreadHandler import io.bitdrift.capture.common.MainThreadHandler import io.bitdrift.capture.events.span.Span import io.bitdrift.capture.events.span.SpanResult @@ -22,6 +23,7 @@ import io.bitdrift.capture.providers.SystemDateProvider import io.bitdrift.capture.providers.session.SessionStrategy import io.bitdrift.capture.reports.FatalIssueMechanism import io.bitdrift.capture.reports.FatalIssueReporter +import io.bitdrift.capture.threading.CaptureDispatchers import okhttp3.HttpUrl import java.util.UUID import java.util.concurrent.atomic.AtomicReference @@ -35,14 +37,17 @@ internal sealed class LoggerState { /** * The logger is in the process of being started. Subsequent attempts to start the logger will be ignored. + * Any calls to Logger.log() meanwhile Logger.start() is in process will be cached in memory */ - data object Starting : LoggerState() + class Starting( + val preInitInMemoryLogger: PreInitInMemoryLogger, + ) : LoggerState() /** * The logger has been successfully started and is ready for use. Subsequent attempts to start the logger will be ignored. */ class Started( - val logger: LoggerImpl, + val loggerImpl: LoggerImpl, ) : LoggerState() /** @@ -58,6 +63,7 @@ object Capture { internal const val LOG_TAG = "BitdriftCapture" private val default: AtomicReference = AtomicReference(LoggerState.NotStarted) private val fatalIssueReporter = FatalIssueReporter() + private val preInitInMemoryLogger by lazy { PreInitInMemoryLogger() } /** * Returns a handle to the underlying logger instance, if Capture has been started. @@ -67,8 +73,8 @@ object Capture { fun logger(): ILogger? = when (val state = default.get()) { is LoggerState.NotStarted -> null - is LoggerState.Starting -> null - is LoggerState.Started -> state.logger + is LoggerState.Starting -> state.preInitInMemoryLogger + is LoggerState.Started -> state.loggerImpl is LoggerState.StartFailure -> null } @@ -169,6 +175,7 @@ object Capture { dateProvider: DateProvider? = null, apiUrl: HttpUrl = defaultCaptureApiUrl, bridge: IBridge, + backgroundThreadHandler: IBackgroundThreadHandler = CaptureDispatchers.CommonBackground, ) { // Note that we need to use @Synchronized to prevent multiple loggers from being initialized, // while subsequent logger access relies on volatile reads. @@ -184,23 +191,27 @@ object Capture { } // Ideally we would use `getAndUpdate` in here but it's available for API 24 and up only. - if (default.compareAndSet(LoggerState.NotStarted, LoggerState.Starting)) { - try { - val logger = - LoggerImpl( - apiKey = apiKey, - apiUrl = apiUrl, - fieldProviders = fieldProviders, - dateProvider = dateProvider ?: SystemDateProvider(), - configuration = configuration, - sessionStrategy = sessionStrategy, - bridge = bridge, - fatalIssueReporter = fatalIssueReporter, - ) - default.set(LoggerState.Started(logger)) - } catch (e: Throwable) { - Log.w(LOG_TAG, "Failed to start Capture", e) - default.set(LoggerState.StartFailure) + if (default.compareAndSet(LoggerState.NotStarted, LoggerState.Starting(preInitInMemoryLogger))) { + backgroundThreadHandler.runAsync { + try { + val loggerImpl = + LoggerImpl( + apiKey = apiKey, + apiUrl = apiUrl, + fieldProviders = fieldProviders, + dateProvider = dateProvider ?: SystemDateProvider(), + configuration = configuration, + sessionStrategy = sessionStrategy, + bridge = bridge, + fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitInMemoryLogger, + ) + default.set(LoggerState.Started(loggerImpl)) + } catch (e: Throwable) { + Log.w(LOG_TAG, "Failed to start Capture", e) + preInitInMemoryLogger.clear() + default.set(LoggerState.StartFailure) + } } } else { Log.w(LOG_TAG, "Multiple attempts to start Capture") diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IPreInitLogFlusher.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IPreInitLogFlusher.kt new file mode 100644 index 000000000..ed9cc8e1d --- /dev/null +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/IPreInitLogFlusher.kt @@ -0,0 +1,15 @@ +// 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 + +/** + * Flushes the internal memory logging into the loggerImpl + */ +fun interface IPreInitLogFlusher { + /** Flush all in memory Logger calls into the Native `LoggerImpl` **/ + fun flushToNative(loggerImpl: ILogger) +} 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 f02025fa7..c5af72ce0 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 @@ -85,6 +85,7 @@ internal class LoggerImpl( private val eventListenerDispatcher: CaptureDispatchers.CommonBackground = CaptureDispatchers.CommonBackground, windowManager: IWindowManager = WindowManager(errorHandler), private val fatalIssueReporter: IFatalIssueReporter, + private val preInitLogFlusher: IPreInitLogFlusher, ) : ILogger { private val metadataProvider: MetadataProvider private val batteryMonitor = BatteryMonitor(context) @@ -261,6 +262,8 @@ internal class LoggerImpl( appExitLogger.installAppExitLogger() CaptureJniLibrary.startLogger(this.loggerId) + + preInitLogFlusher.flushToNative(this) } writeSdkStartLog(context, clientAttributes, initDuration = duration) diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt new file mode 100644 index 000000000..8450023b3 --- /dev/null +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt @@ -0,0 +1,120 @@ +// 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.annotation.OpenForTesting +import io.bitdrift.capture.events.span.Span +import io.bitdrift.capture.network.HttpRequestInfo +import io.bitdrift.capture.network.HttpResponseInfo +import java.util.UUID +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.time.Duration + +/** + * + * Given that Logger.start() runs on dedicated thread, will be caching any calls to Logger.log while + * start is in process of being initialized. + * + * When Logger.start() is completed will flush the in memory calls to the native layer + */ +internal class PreInitInMemoryLogger : + ILogger, + IPreInitLogFlusher { + private val _bufferedLoggerCalls = ConcurrentLinkedQueue<(ILogger) -> Unit>() + + @get:OpenForTesting + val bufferedLoggerCalls: List<(ILogger) -> Unit> + get() = _bufferedLoggerCalls.toList() + + override val sessionId: String = DEFAULT_NOT_SETUP_MESSAGE + + override val sessionUrl: String = DEFAULT_NOT_SETUP_MESSAGE + + override val deviceId: String = DEFAULT_NOT_SETUP_MESSAGE + + /** Flush all in memory Logger calls into the Native `LoggerImpl` **/ + override fun flushToNative(loggerImpl: ILogger) { + _bufferedLoggerCalls.forEach { it(loggerImpl) } + _bufferedLoggerCalls.clear() + } + + override fun startNewSession() { + addLoggerCall { it.startNewSession() } + } + + override fun createTemporaryDeviceCode(completion: (CaptureResult) -> Unit) { + addLoggerCall { it.createTemporaryDeviceCode(completion) } + } + + override fun addField( + key: String, + value: String, + ) { + addLoggerCall { it.addField(key, value) } + } + + override fun removeField(key: String) { + addLoggerCall { it.removeField(key) } + } + + override fun log( + level: LogLevel, + fields: Map?, + throwable: Throwable?, + message: () -> String, + ) { + addLoggerCall { it.log(level, fields, throwable, message) } + } + + override fun logAppLaunchTTI(duration: Duration) { + addLoggerCall { it.logAppLaunchTTI(duration) } + } + + override fun logScreenView(screenName: String) { + addLoggerCall { it.logScreenView(screenName) } + } + + override fun startSpan( + name: String, + level: LogLevel, + fields: Map?, + startTimeMs: Long?, + parentSpanId: UUID?, + ): Span { + val span = Span(null, name, level, fields, startTimeMs, parentSpanId) + addLoggerCall { + span.setLoggerImpl(it as LoggerImpl) + it.startSpan(name, level, fields, startTimeMs, parentSpanId) + } + return span + } + + override fun log(httpRequestInfo: HttpRequestInfo) { + addLoggerCall { it.log(httpRequestInfo) } + } + + override fun log(httpResponseInfo: HttpResponseInfo) { + addLoggerCall { it.log(httpResponseInfo) } + } + + /** Clear stored Logger calls **/ + fun clear() { + _bufferedLoggerCalls.clear() + } + + private fun addLoggerCall(logCall: (ILogger) -> Unit) { + if (_bufferedLoggerCalls.size >= MAX_LOG_CALL_SIZE) { + _bufferedLoggerCalls.poll() + } + _bufferedLoggerCalls.add(logCall) + } + + private companion object { + private const val DEFAULT_NOT_SETUP_MESSAGE = "SDK starting" + private const val MAX_LOG_CALL_SIZE = 512 // Matching the pre-config buffer definition + } +} diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/span/Span.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/span/Span.kt index f6f9d4b7e..cea30ac21 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/span/Span.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/span/Span.kt @@ -130,4 +130,11 @@ class Span internal constructor( } logger = null } + + /** + * This is only needed during [io.bitdrift.capture.PreInitInMemoryLogger] + */ + internal fun setLoggerImpl(loggerImpl: LoggerImpl) { + this.logger = loggerImpl + } } diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt index 528b4b799..5a20df1bc 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt @@ -17,6 +17,7 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import io.bitdrift.capture.fakes.FakePreInitLogFlusher import io.bitdrift.capture.providers.DateProvider import io.bitdrift.capture.providers.session.SessionStrategy import io.bitdrift.capture.reports.FatalIssueReporter @@ -51,6 +52,7 @@ class CaptureLoggerSessionOverrideTest { private lateinit var logger: LoggerImpl private var testServerPort: Int? = null private val fatalIssueReporter = FatalIssueReporter() + private val preInitLogFlusher = FakePreInitLogFlusher() @Before fun setUp() { @@ -88,6 +90,7 @@ class CaptureLoggerSessionOverrideTest { configuration = Configuration(), preferences = preferences, fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) CaptureTestJniLibrary.stopTestApiServer() @@ -121,8 +124,8 @@ class CaptureLoggerSessionOverrideTest { preferences = preferences, activityManager = activityManager, fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) - val newStreamId = CaptureTestJniLibrary.awaitNextApiStream() assertThat(newStreamId).isNotEqualTo(-1) diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt index 2a82a3ecc..aba89210b 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt @@ -21,6 +21,7 @@ import io.bitdrift.capture.attributes.DeviceAttributes import io.bitdrift.capture.attributes.NetworkAttributes import io.bitdrift.capture.common.RuntimeFeature import io.bitdrift.capture.fakes.FakeFatalIssueReporter +import io.bitdrift.capture.fakes.FakePreInitLogFlusher import io.bitdrift.capture.network.HttpRequestInfo import io.bitdrift.capture.network.HttpResponse import io.bitdrift.capture.network.HttpResponseInfo @@ -63,6 +64,7 @@ class CaptureLoggerTest { private lateinit var logger: LoggerImpl private var testServerPort: Int? = null private val fatalIssueReporter: IFatalIssueReporter = FakeFatalIssueReporter() + private val preInitLogFlusher = FakePreInitLogFlusher() @Before fun setUp() { @@ -74,6 +76,7 @@ class CaptureLoggerTest { testServerPort = CaptureTestJniLibrary.startTestApiServer(-1) + preInitLogFlusher.reset() logger = buildLogger(dateProvider = systemDateProvider) } @@ -441,6 +444,7 @@ class CaptureLoggerTest { ) assertThat(JniRuntime(logger.loggerId).isEnabled(RuntimeFeature.SESSION_REPLAY_COMPOSE)).isFalse + assertThat(preInitLogFlusher.wasFlushed).isTrue() } private fun testServerUrl(): HttpUrl = @@ -469,6 +473,7 @@ class CaptureLoggerTest { dateProvider = dateProvider, configuration = Configuration(), fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) } 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 index e4bc866ca..7a5a372d3 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ConfigurationTest.kt @@ -13,6 +13,7 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import io.bitdrift.capture.fakes.FakeBackgroundThreadHandler import io.bitdrift.capture.providers.session.SessionStrategy import org.assertj.core.api.Assertions import org.junit.After @@ -24,6 +25,8 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [21]) class ConfigurationTest { + private val fakeBackgroundThreadHandler = FakeBackgroundThreadHandler() + @Test fun configurationFailure() { val initializer = ContextHolder() @@ -56,6 +59,7 @@ class ConfigurationTest { sessionStrategy = SessionStrategy.Fixed(), dateProvider = null, bridge = bridge, + backgroundThreadHandler = fakeBackgroundThreadHandler, ) // The configuration failed so the logger is still `null`. @@ -85,6 +89,7 @@ class ConfigurationTest { sessionStrategy = SessionStrategy.Fixed(), dateProvider = null, bridge = bridge, + backgroundThreadHandler = fakeBackgroundThreadHandler, ) Assertions.assertThat(Capture.logger()).isNull() diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt index 19772b9fe..6c530e604 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt @@ -13,6 +13,7 @@ import com.google.gson.reflect.TypeToken import io.bitdrift.capture.error.ErrorReportRequest import io.bitdrift.capture.error.ErrorReporterService import io.bitdrift.capture.fakes.FakeFatalIssueReporter +import io.bitdrift.capture.fakes.FakePreInitLogFlusher import io.bitdrift.capture.network.okhttp.OkHttpApiClient import io.bitdrift.capture.providers.FieldProvider import io.bitdrift.capture.providers.SystemDateProvider @@ -36,6 +37,7 @@ class ErrorReporterTest { private lateinit var server: MockWebServer private lateinit var reporter: ErrorReporterService private val fatalIssueReporter: IFatalIssueReporter = FakeFatalIssueReporter() + private val preInitLogFlusher = FakePreInitLogFlusher() init { CaptureJniLibrary.load() @@ -110,6 +112,7 @@ class ErrorReporterTest { configuration = Configuration(), errorReporter = reporter, fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) val errorHandler = ErrorHandler() diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt index 6e46699cc..e79e7c1db 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt @@ -180,8 +180,7 @@ class FatalIssueReporterTest { FatalIssueReporterState.ProcessingFailure::class.java, ) val state = - fatalIssueReporter.fatalIssueReporterStatus.state - as FatalIssueReporterState.ProcessingFailure + fatalIssueReporter.fatalIssueReporterStatus.state as FatalIssueReporterState.ProcessingFailure assertThat(state.fatalIssueMechanism).isEqualTo(FatalIssueMechanism.BuiltIn) assertThat(state.errorMessage) .isEqualTo("Error while initializing reporter for BuiltIn mode. Fake JVM exception") diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt new file mode 100644 index 000000000..049d678d4 --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.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 io.bitdrift.capture.events.span.Span +import io.bitdrift.capture.network.HttpRequestInfo +import io.bitdrift.capture.network.HttpResponseInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import java.util.UUID +import kotlin.time.Duration + +class PreInitInMemoryLoggerTest { + private val preInitInMemoryLogger = PreInitInMemoryLogger() + private val testLogger = TestLogger() + + @Before + fun setup() { + preInitInMemoryLogger.clear() + testLogger.clear() + } + + @Test + fun log_withoutOverwriteOldest_shouldMatchExpectedCalls() { + val totalCalls = 100 + + triggerScreenViewCalls(totalCalls = totalCalls) + + assertThat(testLogger.screenNameViewed.size).isEqualTo(totalCalls) + assertThat(testLogger.screenNameViewed).last().isEqualTo("Screen Viewed 100") + } + + @Test + fun log_withMaxSizeReached_shouldRemoveFirstEntryAndKeepLast() { + val totalCalls = 2048 + + triggerScreenViewCalls(totalCalls = totalCalls) + + assertThat(testLogger.screenNameViewed.size).isEqualTo(512) + assertThat(testLogger.screenNameViewed).first().isEqualTo("Screen Viewed 1537") + assertThat(testLogger.screenNameViewed).last().isEqualTo("Screen Viewed 2048") + } + + private fun triggerScreenViewCalls(totalCalls: Int) { + for (i in 1..totalCalls) { + preInitInMemoryLogger.logScreenView("Screen Viewed $i") + } + + preInitInMemoryLogger.bufferedLoggerCalls.forEach { it(testLogger) } + } + + @Suppress("EmptyFunctionBlock") + private class TestLogger : ILogger { + val screenNameViewed = mutableListOf() + + override val sessionId: String = "test-session" + + override val sessionUrl: String = "test-url" + + override val deviceId: String = "test-device" + + override fun startNewSession() {} + + override fun createTemporaryDeviceCode(completion: (CaptureResult) -> Unit) {} + + override fun addField( + key: String, + value: String, + ) {} + + override fun removeField(key: String) {} + + override fun logScreenView(screenName: String) { + screenNameViewed.add(screenName) + } + + override fun log( + level: LogLevel, + fields: Map?, + throwable: Throwable?, + message: () -> String, + ) {} + + override fun logAppLaunchTTI(duration: Duration) {} + + override fun startSpan( + name: String, + level: LogLevel, + fields: Map?, + startTimeMs: Long?, + parentSpanId: UUID?, + ): Span = Span(null, name, level, fields, startTimeMs, parentSpanId) + + override fun log(httpRequestInfo: HttpRequestInfo) {} + + override fun log(httpResponseInfo: HttpResponseInfo) {} + + fun clear() { + screenNameViewed.clear() + } + } +} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt index 2817996b7..7287afb07 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt @@ -10,6 +10,7 @@ package io.bitdrift.capture import androidx.test.core.app.ApplicationProvider import com.nhaarman.mockitokotlin2.mock import io.bitdrift.capture.fakes.FakeFatalIssueReporter +import io.bitdrift.capture.fakes.FakePreInitLogFlusher import io.bitdrift.capture.providers.session.SessionStrategy import io.bitdrift.capture.reports.IFatalIssueReporter import okhttp3.HttpUrl @@ -26,6 +27,7 @@ import java.util.concurrent.CountDownLatch @Config(sdk = [21]) class SessionStrategyTest { private val fatalIssueReporter: IFatalIssueReporter = FakeFatalIssueReporter() + private val preInitLogFlusher = FakePreInitLogFlusher() @Before fun setUp() { @@ -51,6 +53,7 @@ class SessionStrategyTest { }, configuration = Configuration(), fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) val sessionId = logger.sessionId @@ -85,6 +88,7 @@ class SessionStrategyTest { configuration = Configuration(), preferences = mock(), fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) val sessionId = logger.sessionId diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt index 3bf9c3112..502c39423 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt @@ -9,6 +9,7 @@ package io.bitdrift.capture import androidx.test.core.app.ApplicationProvider import io.bitdrift.capture.fakes.FakeFatalIssueReporter +import io.bitdrift.capture.fakes.FakePreInitLogFlusher import io.bitdrift.capture.providers.SystemDateProvider import io.bitdrift.capture.providers.session.SessionStrategy import io.bitdrift.capture.reports.IFatalIssueReporter @@ -24,6 +25,7 @@ import org.robolectric.annotation.Config @Config(sdk = [21]) class SessionUrlTest { private val fatalIssueReporter: IFatalIssueReporter = FakeFatalIssueReporter() + private val preInitLogFlusher = FakePreInitLogFlusher() @Before fun setUp() { @@ -76,5 +78,6 @@ class SessionUrlTest { dateProvider = SystemDateProvider(), sessionStrategy = SessionStrategy.Fixed { "SESSION_ID" }, fatalIssueReporter = fatalIssueReporter, + preInitLogFlusher = preInitLogFlusher, ) } diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakePreInitLogFlusher.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakePreInitLogFlusher.kt new file mode 100644 index 000000000..01bc4440c --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakePreInitLogFlusher.kt @@ -0,0 +1,27 @@ +// 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.fakes + +import io.bitdrift.capture.ILogger +import io.bitdrift.capture.IPreInitLogFlusher + +/** + * Fake [IPreInitLogFlusher] to ease testing + */ +class FakePreInitLogFlusher : IPreInitLogFlusher { + private var _wasFlushed = false + val wasFlushed: Boolean + get() = _wasFlushed + + override fun flushToNative(loggerImpl: ILogger) { + _wasFlushed = true + } + + fun reset() { + _wasFlushed = false + } +} diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ComposeScreen.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ComposeScreen.kt index f50a3a0f1..a942a8869 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ComposeScreen.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ComposeScreen.kt @@ -25,11 +25,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.text.HtmlCompat +import io.bitdrift.capture.Capture +import io.bitdrift.capture.LogLevel +import io.bitdrift.capture.events.span.SpanResult import io.bitdrift.capture.replay.compose.CaptureModifier.captureIgnore import timber.log.Timber @Composable fun SecondScreen() { + val span = Capture.Logger.startSpan("ComposeScreen", LogLevel.INFO) Surface { val smallSpacing = Dp(value = 5f) val normalSpacing = Dp(value = 10f) @@ -68,6 +72,7 @@ fun SecondScreen() { } } } + span?.end(SpanResult.SUCCESS) } @Composable diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt index f03086226..3609eb7bb 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt @@ -11,12 +11,10 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.webkit.WebView import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.Toast @@ -38,6 +36,8 @@ import io.bitdrift.capture.Capture.Logger import io.bitdrift.capture.Error import io.bitdrift.capture.LogLevel import io.bitdrift.capture.apollo.CaptureApolloInterceptor +import io.bitdrift.capture.events.span.Span +import io.bitdrift.capture.events.span.SpanResult import io.bitdrift.capture.network.okhttp.CaptureOkHttpEventListenerFactory import io.bitdrift.gradletestapp.databinding.FragmentFirstBinding import kotlinx.coroutines.MainScope @@ -83,12 +83,15 @@ class FirstFragment : Fragment() { private lateinit var clipboardManager: ClipboardManager private lateinit var okHttpClient: OkHttpClient private lateinit var apolloClient: ApolloClient + private var firstFragmentToCopySessionSpan: Span ?= null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + firstFragmentToCopySessionSpan = Logger.startSpan("CreateFragmentToCopySessionClick", LogLevel.INFO) + _binding = FragmentFirstBinding.inflate(inflater, container, false) val viewRoot = binding.root @@ -160,6 +163,7 @@ class FirstFragment : Fragment() { private fun copySessionUrl(view: View) { val data = ClipData.newPlainText("sessionUrl", Logger.sessionUrl) clipboardManager.setPrimaryClip(data) + firstFragmentToCopySessionSpan?.end(SpanResult.SUCCESS) } private fun startNewSession(view: View) { diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt index c6a657b80..e65254e95 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt @@ -44,6 +44,7 @@ import android.app.ApplicationStartInfo.START_TYPE_UNSET import android.app.ApplicationStartInfo.START_TYPE_WARM import android.content.Context import android.content.SharedPreferences +import android.graphics.Paint.Cap import android.os.Build import android.os.Bundle import android.os.StrictMode @@ -51,6 +52,7 @@ import android.util.Log import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Logger import io.bitdrift.capture.Capture import io.bitdrift.capture.Capture.Logger.sessionUrl import io.bitdrift.capture.LogLevel diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/WebViewFragment.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/WebViewFragment.kt index e9343534a..46c34da44 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/WebViewFragment.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/WebViewFragment.kt @@ -13,6 +13,9 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebView import androidx.fragment.app.Fragment +import io.bitdrift.capture.Capture +import io.bitdrift.capture.LogLevel +import io.bitdrift.capture.events.span.SpanResult /** * A basic WebView that can be used to test multi process. @@ -23,9 +26,11 @@ class WebViewFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + val span = Capture.Logger.startSpan("WebViewFragment", LogLevel.INFO) val view = inflater.inflate(R.layout.fragment_web_view, container, false) val webView = view.findViewById(R.id.webView) webView.loadUrl("https://bitdrift.io/") + span?.end(SpanResult.SUCCESS) return view } } \ No newline at end of file