diff --git a/android/measure/api/measure.api b/android/measure/api/measure.api index 1e81ab06b..a7993d0e0 100644 --- a/android/measure/api/measure.api +++ b/android/measure/api/measure.api @@ -80,6 +80,7 @@ public abstract interface class sh/measure/android/tracing/Span { public abstract fun getDuration ()J public abstract fun getName ()Ljava/lang/String; public abstract fun getParentId ()Ljava/lang/String; + public abstract fun getSessionId ()Ljava/lang/String; public abstract fun getSpanId ()Ljava/lang/String; public abstract fun getStartTime ()J public abstract fun getStatus ()Lsh/measure/android/tracing/SpanStatus; diff --git a/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt b/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt index c2145a7be..12b6d3c38 100644 --- a/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt +++ b/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt @@ -68,7 +68,9 @@ import sh.measure.android.storage.FileStorage import sh.measure.android.storage.FileStorageImpl import sh.measure.android.storage.PrefsStorage import sh.measure.android.storage.PrefsStorageImpl +import sh.measure.android.tracing.MsrSpanProcessor import sh.measure.android.tracing.MsrTracer +import sh.measure.android.tracing.SpanProcessor import sh.measure.android.tracing.Tracer import sh.measure.android.utils.AndroidSystemClock import sh.measure.android.utils.AndroidTimeProvider @@ -357,10 +359,13 @@ internal class MeasureInitializerImpl( sessionManager = sessionManager, configProvider = configProvider, ), + private val spanProcessor: SpanProcessor = MsrSpanProcessor(eventProcessor), override val tracer: Tracer = MsrTracer( logger = logger, idProvider = idProvider, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, ), ) : MeasureInitializer diff --git a/android/measure/src/main/java/sh/measure/android/events/EventProcessor.kt b/android/measure/src/main/java/sh/measure/android/events/EventProcessor.kt index 7d6c41009..c992f4238 100644 --- a/android/measure/src/main/java/sh/measure/android/events/EventProcessor.kt +++ b/android/measure/src/main/java/sh/measure/android/events/EventProcessor.kt @@ -14,6 +14,7 @@ import sh.measure.android.logger.Logger import sh.measure.android.screenshot.ScreenshotCollector import sh.measure.android.storage.EventStore import sh.measure.android.tracing.InternalTrace +import sh.measure.android.tracing.SpanData import sh.measure.android.utils.IdProvider import sh.measure.android.utils.iso8601Timestamp import java.util.concurrent.RejectedExecutionException @@ -87,6 +88,8 @@ internal interface EventProcessor { attributes: MutableMap = mutableMapOf(), attachments: MutableList = mutableListOf(), ) + + fun trackSpan(spanData: SpanData) } internal class EventProcessorImpl( @@ -166,6 +169,13 @@ internal class EventProcessorImpl( } ?: logger.log(LogLevel.Debug, "Event dropped: $type") } + override fun trackSpan(spanData: SpanData) { + ioExecutor.submit { + eventStore.store(spanData, sessionManager.getSessionId()) + logger.log(LogLevel.Info, "Span processed: ${spanData.name}") + } + } + private fun track( data: T, timestamp: Long, diff --git a/android/measure/src/main/java/sh/measure/android/storage/Database.kt b/android/measure/src/main/java/sh/measure/android/storage/Database.kt index 781e05577..bc894c7e1 100644 --- a/android/measure/src/main/java/sh/measure/android/storage/Database.kt +++ b/android/measure/src/main/java/sh/measure/android/storage/Database.kt @@ -11,6 +11,7 @@ import sh.measure.android.exporter.AttachmentPacket import sh.measure.android.exporter.EventPacket import sh.measure.android.logger.LogLevel import sh.measure.android.logger.Logger +import sh.measure.android.tracing.SpanData import java.io.Closeable internal interface Database : Closeable { @@ -196,6 +197,11 @@ internal interface Database : Closeable { * Clears app exit for sessions which happened before the given [timestamp]. */ fun clearAppExitSessionsBefore(timestamp: Long) + + /** + * Inserts a span into span table. + */ + fun insertSpan(sessionId: String, spanData: SpanData): Boolean } /** @@ -215,6 +221,7 @@ internal class DatabaseImpl( db.execSQL(Sql.CREATE_EVENTS_BATCH_TABLE) db.execSQL(Sql.CREATE_USER_DEFINED_ATTRIBUTES_TABLE) db.execSQL(Sql.CREATE_APP_EXIT_TABLE) + db.execSQL(Sql.CREATE_SPANS_TABLE) db.execSQL(Sql.CREATE_EVENTS_TIMESTAMP_INDEX) db.execSQL(Sql.CREATE_EVENTS_SESSION_ID_INDEX) db.execSQL(Sql.CREATE_EVENTS_BATCH_EVENT_ID_INDEX) @@ -831,6 +838,26 @@ internal class DatabaseImpl( logger.log(LogLevel.Debug, "Cleared $result app_exit rows") } + override fun insertSpan(sessionId: String, spanData: SpanData): Boolean { + val values = ContentValues().apply { + put(SpansTable.COL_NAME, spanData.name) + put(SpansTable.COL_SESSION_ID, sessionId) + put(SpansTable.COL_SPAN_ID, spanData.spanId) + put(SpansTable.COL_TRACE_ID, spanData.traceId) + put(SpansTable.COL_PARENT_ID, spanData.parentId) + put(SpansTable.COL_START_TIME, spanData.startTime) + put(SpansTable.COL_END_TIME, spanData.endTime) + put(SpansTable.COL_DURATION, spanData.duration) + put(SpansTable.COL_STATUS, spanData.status.name) + } + val result = writableDatabase.insert( + SpansTable.TABLE_NAME, + null, + values, + ) + return result != -1L + } + override fun getAttachmentsForEvents(events: List): List { val attachmentIds = mutableListOf() readableDatabase.rawQuery(Sql.getAttachmentsForEvents(events), null).use { diff --git a/android/measure/src/main/java/sh/measure/android/storage/DbConstants.kt b/android/measure/src/main/java/sh/measure/android/storage/DbConstants.kt index de1417f1d..3951c9bf6 100644 --- a/android/measure/src/main/java/sh/measure/android/storage/DbConstants.kt +++ b/android/measure/src/main/java/sh/measure/android/storage/DbConstants.kt @@ -8,6 +8,7 @@ internal object DbConstants { internal object DbVersion { const val V1 = 1 const val V2 = 2 + const val V3 = 3 } internal object EventTable { @@ -64,6 +65,19 @@ internal object AppExitTable { const val COL_CREATED_AT = "created_at" } +internal object SpansTable { + const val TABLE_NAME = "spans" + const val COL_NAME = "name" + const val COL_SESSION_ID = "session_id" + const val COL_SPAN_ID = "span_id" + const val COL_TRACE_ID = "trace_id" + const val COL_PARENT_ID = "parent_id" + const val COL_START_TIME = "start_time" + const val COL_END_TIME = "end_time" + const val COL_DURATION = "duration" + const val COL_STATUS = "status" +} + internal object UserDefinedAttributesTable { const val TABLE_NAME = "user_defined_attributes" const val COL_KEY = "key" @@ -144,6 +158,20 @@ internal object Sql { ) """ + const val CREATE_SPANS_TABLE = """ + CREATE TABLE IF NOT EXISTS ${SpansTable.TABLE_NAME} ( + ${SpansTable.COL_SPAN_ID} TEXT NOT NULL PRIMARY KEY, + ${SpansTable.COL_NAME} TEXT NOT NULL, + ${SpansTable.COL_SESSION_ID} TEXT NOT NULL, + ${SpansTable.COL_TRACE_ID} TEXT NOT NULL, + ${SpansTable.COL_PARENT_ID} TEXT, + ${SpansTable.COL_START_TIME} INTEGER NOT NULL, + ${SpansTable.COL_END_TIME} INTEGER NOT NULL, + ${SpansTable.COL_DURATION} INTEGER NOT NULL, + ${SpansTable.COL_STATUS} TEXT NOT NULL + ) + """ + const val CREATE_SESSIONS_CREATED_AT_INDEX = """ CREATE INDEX IF NOT EXISTS sessions_created_at_index ON ${SessionsTable.TABLE_NAME} (${SessionsTable.COL_CREATED_AT}) """ diff --git a/android/measure/src/main/java/sh/measure/android/storage/DbMigrations.kt b/android/measure/src/main/java/sh/measure/android/storage/DbMigrations.kt index 677abdcb8..a79771119 100644 --- a/android/measure/src/main/java/sh/measure/android/storage/DbMigrations.kt +++ b/android/measure/src/main/java/sh/measure/android/storage/DbMigrations.kt @@ -13,6 +13,7 @@ internal object DbMigrations { for (version in oldVersion + 1..newVersion) { when (version) { DbVersion.V2 -> migrateToV2(db) + DbVersion.V3 -> migrateToV3(db) else -> logger.log( LogLevel.Warning, "No migration found for version $version", @@ -40,4 +41,8 @@ internal object DbMigrations { """.trimIndent(), ) } + + private fun migrateToV3(db: SQLiteDatabase) { + db.execSQL(Sql.CREATE_SPANS_TABLE) + } } diff --git a/android/measure/src/main/java/sh/measure/android/storage/EventStore.kt b/android/measure/src/main/java/sh/measure/android/storage/EventStore.kt index a7b0b997c..83f812c68 100644 --- a/android/measure/src/main/java/sh/measure/android/storage/EventStore.kt +++ b/android/measure/src/main/java/sh/measure/android/storage/EventStore.kt @@ -9,11 +9,13 @@ import sh.measure.android.events.EventType import sh.measure.android.logger.LogLevel import sh.measure.android.logger.Logger import sh.measure.android.okhttp.HttpData +import sh.measure.android.tracing.SpanData import sh.measure.android.utils.IdProvider import java.io.File internal interface EventStore { fun store(event: Event) + fun store(spanData: SpanData, sessionId: String) } /** @@ -31,6 +33,13 @@ internal class EventStoreImpl( private val idProvider: IdProvider, ) : EventStore { + override fun store(spanData: SpanData, sessionId: String) { + val result = database.insertSpan(sessionId, spanData) + if (!result) { + logger.log(LogLevel.Error, "Unable to store span(${spanData.name}) to database") + } + } + override fun store(event: Event) { val serializedAttributes = event.serializeAttributes() val serializedUserDefAttributes = event.serializeUserDefinedAttributes() diff --git a/android/measure/src/main/java/sh/measure/android/tracing/InvalidSpan.kt b/android/measure/src/main/java/sh/measure/android/tracing/InvalidSpan.kt index 1dc8e7571..16c2a4d88 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/InvalidSpan.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/InvalidSpan.kt @@ -6,6 +6,9 @@ internal class InvalidSpan : Span { override val name: String = "invalid" override val parentId: String? = null + + override val sessionId: String = "" + override val startTime: Long = 0 override fun getStatus(): SpanStatus { diff --git a/android/measure/src/main/java/sh/measure/android/tracing/MsrSpan.kt b/android/measure/src/main/java/sh/measure/android/tracing/MsrSpan.kt index aa588aed3..6a1255eaf 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/MsrSpan.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/MsrSpan.kt @@ -1,6 +1,6 @@ package sh.measure.android.tracing -import android.util.Log +import sh.measure.android.SessionManager import sh.measure.android.logger.LogLevel import sh.measure.android.logger.Logger import sh.measure.android.utils.IdProvider @@ -12,23 +12,26 @@ import sh.measure.android.utils.TimeProvider internal class MsrSpan( private val logger: Logger, private val timeProvider: TimeProvider, + private val spanProcessor: SpanProcessor, override val name: String, override val spanId: String, override val traceId: String, override val parentId: String?, + override val sessionId: String, override val startTime: Long, -) : Span { +) : Span, ReadableSpan { private val lock = Any() private var status = SpanStatus.Unset private var endTime = 0L private var hasEnded: EndState = EndState.NotEnded - private var duration: Long = 0 companion object { fun startSpan( name: String, logger: Logger, timeProvider: TimeProvider, + spanProcessor: SpanProcessor, + sessionManager: SessionManager, idProvider: IdProvider, parentSpan: Span?, timestamp: Long? = null, @@ -36,15 +39,20 @@ internal class MsrSpan( val startTime = timestamp ?: timeProvider.now() val spanId: String = idProvider.spanId() val traceId = parentSpan?.traceId ?: idProvider.traceId() - return MsrSpan( + val sessionId = sessionManager.getSessionId() + val span = MsrSpan( logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, name = name, spanId = spanId, traceId = traceId, parentId = parentSpan?.spanId, + sessionId = sessionId, startTime = startTime, ) + spanProcessor.onStart(span) + return span } } @@ -84,14 +92,11 @@ internal class MsrSpan( endTime = timestamp hasEnded = EndState.Ending } - - // trigger onEnding + spanProcessor.onEnding(this) synchronized(lock) { hasEnded = EndState.Ended } - - // trigger onEnded - Log.i("MsrSpan", "${this.toSpanData()}") + spanProcessor.onEnded(this) } override fun makeCurrent(): Scope { @@ -103,28 +108,43 @@ internal class MsrSpan( } override fun getDuration(): Long { - return duration - } - - private enum class EndState { - NotEnded, - Ending, - Ended, + synchronized(lock) { + if (hasEnded != EndState.Ended) { + logger.log( + LogLevel.Warning, + "Attempt to duration of a span($name) that has not ended", + ) + return 0 + } else { + return calculateDuration() + } + } } - private fun toSpanData(): SpanData { + override fun toSpanData(): SpanData { synchronized(lock) { - this.duration = (endTime - startTime).coerceAtLeast(0) return SpanData( spanId = spanId, + traceId = traceId, name = name, startTime = startTime, endTime = endTime, status = status, hasEnded = hasEnded == EndState.Ended, parentId = parentId, - duration = duration, + sessionId = sessionId, + duration = calculateDuration(), ) } } + + private fun calculateDuration(): Long { + return (endTime - startTime).coerceAtLeast(0) + } + + private enum class EndState { + NotEnded, + Ending, + Ended, + } } diff --git a/android/measure/src/main/java/sh/measure/android/tracing/MsrSpanBuilder.kt b/android/measure/src/main/java/sh/measure/android/tracing/MsrSpanBuilder.kt index 70d4d37ad..3f45845d8 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/MsrSpanBuilder.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/MsrSpanBuilder.kt @@ -1,5 +1,6 @@ package sh.measure.android.tracing +import sh.measure.android.SessionManager import sh.measure.android.logger.Logger import sh.measure.android.utils.IdProvider import sh.measure.android.utils.TimeProvider @@ -8,6 +9,8 @@ internal class MsrSpanBuilder( val name: String, private val idProvider: IdProvider, private val timeProvider: TimeProvider, + private val spanProcessor: SpanProcessor, + private val sessionManager: SessionManager, private val logger: Logger, ) : SpanBuilder { private var parentSpan: Span? = null @@ -26,10 +29,12 @@ internal class MsrSpanBuilder( override fun startSpan(): Span { val parent = findSpanParent() return MsrSpan.startSpan( - name, - logger, - timeProvider, - idProvider, + name = name, + logger = logger, + timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, + idProvider = idProvider, parentSpan = parent, ) } @@ -41,6 +46,8 @@ internal class MsrSpanBuilder( logger = logger, timeProvider = timeProvider, idProvider = idProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, parentSpan = parent, timestamp = timestamp, ) diff --git a/android/measure/src/main/java/sh/measure/android/tracing/MsrTracer.kt b/android/measure/src/main/java/sh/measure/android/tracing/MsrTracer.kt index 8a4d3d58c..2cc1afb5c 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/MsrTracer.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/MsrTracer.kt @@ -1,5 +1,6 @@ package sh.measure.android.tracing +import sh.measure.android.SessionManager import sh.measure.android.logger.Logger import sh.measure.android.utils.IdProvider import sh.measure.android.utils.TimeProvider @@ -8,8 +9,10 @@ internal class MsrTracer( private val logger: Logger, private val idProvider: IdProvider, private val timeProvider: TimeProvider, + private val spanProcessor: SpanProcessor, + private val sessionManager: SessionManager, ) : Tracer { override fun spanBuilder(name: String): SpanBuilder { - return MsrSpanBuilder(name, idProvider, timeProvider, logger) + return MsrSpanBuilder(name, idProvider, timeProvider, spanProcessor, sessionManager, logger) } } diff --git a/android/measure/src/main/java/sh/measure/android/tracing/ReadableSpan.kt b/android/measure/src/main/java/sh/measure/android/tracing/ReadableSpan.kt new file mode 100644 index 000000000..99344879c --- /dev/null +++ b/android/measure/src/main/java/sh/measure/android/tracing/ReadableSpan.kt @@ -0,0 +1,5 @@ +package sh.measure.android.tracing + +internal interface ReadableSpan { + fun toSpanData(): SpanData +} diff --git a/android/measure/src/main/java/sh/measure/android/tracing/Span.kt b/android/measure/src/main/java/sh/measure/android/tracing/Span.kt index fe821b8ab..98ad123d2 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/Span.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/Span.kt @@ -25,6 +25,11 @@ interface Span { */ val parentId: String? + /** + * The session this span is a part of. + */ + val sessionId: String + /** * The time since epoch when the span started. */ @@ -76,7 +81,7 @@ interface Span { fun withScope(block: () -> T): T /** - * The duration of the span once it ends. If the span has not ended, returns 0. + * The duration of the span in milliseconds once it ends. If the span has not ended, returns 0. */ fun getDuration(): Long diff --git a/android/measure/src/main/java/sh/measure/android/tracing/SpanData.kt b/android/measure/src/main/java/sh/measure/android/tracing/SpanData.kt index cdbce870c..9009a3fc3 100644 --- a/android/measure/src/main/java/sh/measure/android/tracing/SpanData.kt +++ b/android/measure/src/main/java/sh/measure/android/tracing/SpanData.kt @@ -2,8 +2,10 @@ package sh.measure.android.tracing internal data class SpanData( val name: String, + val traceId: String, val spanId: String, val parentId: String?, + val sessionId: String, val startTime: Long, val endTime: Long, val duration: Long, diff --git a/android/measure/src/main/java/sh/measure/android/tracing/SpanProcessor.kt b/android/measure/src/main/java/sh/measure/android/tracing/SpanProcessor.kt new file mode 100644 index 000000000..75711e218 --- /dev/null +++ b/android/measure/src/main/java/sh/measure/android/tracing/SpanProcessor.kt @@ -0,0 +1,21 @@ +package sh.measure.android.tracing + +import sh.measure.android.events.EventProcessor + +internal interface SpanProcessor { + fun onStart(span: ReadableSpan) + fun onEnding(span: ReadableSpan) + fun onEnded(span: ReadableSpan) +} + +internal class MsrSpanProcessor(private val eventProcessor: EventProcessor) : SpanProcessor { + override fun onStart(span: ReadableSpan) { + } + + override fun onEnding(span: ReadableSpan) { + } + + override fun onEnded(span: ReadableSpan) { + eventProcessor.trackSpan(span.toSpanData()) + } +} diff --git a/android/measure/src/test/java/sh/measure/android/events/EventProcessorTest.kt b/android/measure/src/test/java/sh/measure/android/events/EventProcessorTest.kt index cb5ecb146..78757ce60 100644 --- a/android/measure/src/test/java/sh/measure/android/events/EventProcessorTest.kt +++ b/android/measure/src/test/java/sh/measure/android/events/EventProcessorTest.kt @@ -474,4 +474,14 @@ internal class EventProcessorTest { // Then assertTrue(sessionManager.onEventTracked) } + + @Test + fun `trackSpan stores span`() { + // When + val spanData = TestData.getSpanData() + eventProcessor.trackSpan(spanData) + + // Then + assertTrue(eventStore.trackedSpans.contains(spanData)) + } } diff --git a/android/measure/src/test/java/sh/measure/android/fakes/FakeEventStore.kt b/android/measure/src/test/java/sh/measure/android/fakes/FakeEventStore.kt index 1df2fb0c9..7b48c40bb 100644 --- a/android/measure/src/test/java/sh/measure/android/fakes/FakeEventStore.kt +++ b/android/measure/src/test/java/sh/measure/android/fakes/FakeEventStore.kt @@ -2,11 +2,17 @@ package sh.measure.android.fakes import sh.measure.android.events.Event import sh.measure.android.storage.EventStore +import sh.measure.android.tracing.SpanData internal class FakeEventStore : EventStore { val trackedEvents = mutableListOf>() + val trackedSpans = mutableListOf() override fun store(event: Event) { trackedEvents.add(event) } + + override fun store(spanData: SpanData, sessionId: String) { + trackedSpans.add(spanData) + } } diff --git a/android/measure/src/test/java/sh/measure/android/fakes/NoopSpanProcessor.kt b/android/measure/src/test/java/sh/measure/android/fakes/NoopSpanProcessor.kt new file mode 100644 index 000000000..57e3bac7a --- /dev/null +++ b/android/measure/src/test/java/sh/measure/android/fakes/NoopSpanProcessor.kt @@ -0,0 +1,18 @@ +package sh.measure.android.fakes + +import sh.measure.android.tracing.ReadableSpan +import sh.measure.android.tracing.SpanProcessor + +internal class NoopSpanProcessor : SpanProcessor { + override fun onStart(span: ReadableSpan) { + // No-op + } + + override fun onEnding(span: ReadableSpan) { + // No-op + } + + override fun onEnded(span: ReadableSpan) { + // No-op + } +} diff --git a/android/measure/src/test/java/sh/measure/android/fakes/TestData.kt b/android/measure/src/test/java/sh/measure/android/fakes/TestData.kt index f4b1554ac..f95faca32 100644 --- a/android/measure/src/test/java/sh/measure/android/fakes/TestData.kt +++ b/android/measure/src/test/java/sh/measure/android/fakes/TestData.kt @@ -19,6 +19,7 @@ import sh.measure.android.lifecycle.AppLifecycleType import sh.measure.android.lifecycle.ApplicationLifecycleData import sh.measure.android.lifecycle.FragmentLifecycleData import sh.measure.android.lifecycle.FragmentLifecycleType +import sh.measure.android.logger.Logger import sh.measure.android.navigation.NavigationData import sh.measure.android.navigation.ScreenViewData import sh.measure.android.networkchange.NetworkChangeData @@ -31,6 +32,11 @@ import sh.measure.android.storage.AttachmentEntity import sh.measure.android.storage.BatchEntity import sh.measure.android.storage.EventEntity import sh.measure.android.storage.SessionEntity +import sh.measure.android.tracing.MsrSpan +import sh.measure.android.tracing.SpanData +import sh.measure.android.tracing.SpanProcessor +import sh.measure.android.tracing.SpanStatus +import sh.measure.android.utils.TimeProvider internal object TestData { @@ -443,4 +449,43 @@ internal object TestData { fun getScreenViewData(): ScreenViewData { return ScreenViewData(name = "screen-name") } + + fun getSpanData(): SpanData { + return SpanData( + name = "span-name", + traceId = "trace-id", + spanId = "span-id", + parentId = "parent-id", + sessionId = "session-id", + startTime = 1000L, + endTime = 2000L, + duration = 1000L, + status = SpanStatus.Ok, + hasEnded = true, + ) + } + + fun getSpan( + logger: Logger, + timeProvider: TimeProvider, + spanProcessor: SpanProcessor, + name: String = "span-name", + spanId: String = "span-id", + traceId: String = "trace-id", + parentId: String? = null, + sessionId: String = "session-id", + startTime: Long = 987654321L, + ): MsrSpan { + return MsrSpan( + logger, + timeProvider, + spanProcessor, + name, + spanId, + traceId, + parentId, + sessionId, + startTime, + ) + } } diff --git a/android/measure/src/test/java/sh/measure/android/storage/DatabaseTest.kt b/android/measure/src/test/java/sh/measure/android/storage/DatabaseTest.kt index 39628a7ab..98abeaa01 100644 --- a/android/measure/src/test/java/sh/measure/android/storage/DatabaseTest.kt +++ b/android/measure/src/test/java/sh/measure/android/storage/DatabaseTest.kt @@ -59,6 +59,8 @@ class DatabaseTest { ) it.moveToNext() assertEquals(AppExitTable.TABLE_NAME, it.getString(it.getColumnIndex("name"))) + it.moveToNext() + assertEquals(SpansTable.TABLE_NAME, it.getString(it.getColumnIndex("name"))) } } @@ -881,6 +883,15 @@ class DatabaseTest { assertEquals(0, count) } + @Test + fun `insertSpan inserts span and returns success`() { + val result = database.insertSpan( + "session-id", + TestData.getSpanData(), + ) + assertTrue(result) + } + private fun queryAllEvents(db: SQLiteDatabase): Cursor { return db.query( EventTable.TABLE_NAME, diff --git a/android/measure/src/test/java/sh/measure/android/storage/EventStoreTest.kt b/android/measure/src/test/java/sh/measure/android/storage/EventStoreTest.kt index 552190998..4162eef42 100644 --- a/android/measure/src/test/java/sh/measure/android/storage/EventStoreTest.kt +++ b/android/measure/src/test/java/sh/measure/android/storage/EventStoreTest.kt @@ -73,6 +73,15 @@ internal class EventStoreTest { assertEquals("fake-file-path", eventEntity.filePath) } + @Test + fun `stores span`() { + val spanData = TestData.getSpanData() + val sessionId = "session-id" + eventStore.store(spanData, sessionId) + + verify(database).insertSpan(sessionId, spanData) + } + @Test fun `given http event contains request body, stores it in file storage and stores the path in database`() { // given diff --git a/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanBuilderTest.kt b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanBuilderTest.kt index fd3255ba0..f2bb37122 100644 --- a/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanBuilderTest.kt +++ b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanBuilderTest.kt @@ -2,7 +2,9 @@ package sh.measure.android.tracing import org.junit.Assert import org.junit.Test +import sh.measure.android.fakes.FakeSessionManager import sh.measure.android.fakes.NoopLogger +import sh.measure.android.fakes.NoopSpanProcessor import sh.measure.android.utils.AndroidTimeProvider import sh.measure.android.utils.IdProviderImpl import sh.measure.android.utils.RandomizerImpl @@ -13,33 +15,73 @@ class MsrSpanBuilderTest { private val testClock = TestClock.create() private val timeProvider = AndroidTimeProvider(testClock) private val logger = NoopLogger() + private val spanProcessor = NoopSpanProcessor() + private val sessionManager = FakeSessionManager() @Test fun `setsParent sets span parent`() { - val parentSpan = MsrSpanBuilder("parent-name", idProvider, timeProvider, logger).startSpan() - val span = - MsrSpanBuilder("span-name", idProvider, timeProvider, logger).setParent(parentSpan) - .startSpan() + val parentSpan = MsrSpanBuilder( + "parent-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).startSpan() + val span = MsrSpanBuilder( + "span-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).setParent(parentSpan).startSpan() Assert.assertEquals(parentSpan.spanId, span.parentId) } @Test fun `sets parent from thread local storage`() { - val parentSpan = MsrSpanBuilder("parent-name", idProvider, timeProvider, logger).startSpan() + val parentSpan = MsrSpanBuilder( + "parent-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).startSpan() parentSpan.makeCurrent().use { - val span = - MsrSpanBuilder("span-name", idProvider, timeProvider, logger).startSpan() + val span = MsrSpanBuilder( + "span-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).startSpan() Assert.assertEquals(parentSpan.spanId, span.parentId) } } @Test fun `setNoParent forces no parent to be set`() { - val parentSpan = MsrSpanBuilder("parent-name", idProvider, timeProvider, logger).startSpan() + val parentSpan = MsrSpanBuilder( + "parent-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).startSpan() parentSpan.makeCurrent().use { - val span = - MsrSpanBuilder("span-name", idProvider, timeProvider, logger).setNoParent().startSpan() + val span = MsrSpanBuilder( + "span-name", + idProvider, + timeProvider, + spanProcessor, + sessionManager, + logger, + ).setNoParent().startSpan() Assert.assertNull(span.parentId) } } diff --git a/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanProcessorTest.kt b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanProcessorTest.kt new file mode 100644 index 000000000..d57cd7ab0 --- /dev/null +++ b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanProcessorTest.kt @@ -0,0 +1,29 @@ +package sh.measure.android.tracing + +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.verify +import sh.measure.android.events.EventProcessorImpl +import sh.measure.android.fakes.NoopLogger +import sh.measure.android.fakes.TestData +import sh.measure.android.utils.AndroidTimeProvider +import sh.measure.android.utils.TestClock + +class MsrSpanProcessorTest { + private val eventProcessor = mock() + private val logger = NoopLogger() + private val timeProvider = AndroidTimeProvider(TestClock.create()) + + @Test + fun `onEnded delegates to event processor`() { + val spanProcessor = MsrSpanProcessor(eventProcessor) + val span = TestData.getSpan( + logger = logger, + timeProvider = timeProvider, + spanProcessor, + ) + spanProcessor.onEnded(span) + + verify(eventProcessor).trackSpan(span.toSpanData()) + } +} diff --git a/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanTest.kt b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanTest.kt index 62e0c8256..21e0e7e9e 100644 --- a/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanTest.kt +++ b/android/measure/src/test/java/sh/measure/android/tracing/MsrSpanTest.kt @@ -2,6 +2,11 @@ package sh.measure.android.tracing import org.junit.Assert import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.verify +import sh.measure.android.fakes.FakeSessionManager import sh.measure.android.fakes.NoopLogger import sh.measure.android.utils.AndroidTimeProvider import sh.measure.android.utils.IdProviderImpl @@ -14,22 +19,28 @@ class MsrSpanTest { private val testClock = TestClock.create() private val timeProvider = AndroidTimeProvider(testClock) private val idProvider = IdProviderImpl(randomizer = RandomizerImpl()) + private val spanProcessor = mock() + private val sessionManager = FakeSessionManager() @Test fun `startSpan sets parent span if provided`() { val parentSpan = MsrSpan( - logger, - timeProvider, - "parent-span", - "span-id", - "trace-id", + logger = logger, + timeProvider = timeProvider, + spanProcessor = spanProcessor, + name = "parent-span", + spanId = "span-id", + traceId = "trace-id", parentId = null, + sessionId = sessionManager.getSessionId(), startTime = 1000, ) val span = MsrSpan.startSpan( "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = parentSpan, timestamp = null, @@ -45,6 +56,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, timestamp = null, @@ -59,6 +72,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, timestamp = timestamp, @@ -66,12 +81,29 @@ class MsrSpanTest { Assert.assertEquals(timestamp, span.startTime) } + @Test + fun `startSpan triggers span processor onStart`() { + val span = MsrSpan.startSpan( + "span-name", + logger = logger, + timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, + idProvider = idProvider, + parentSpan = null, + ) + + verify(spanProcessor, times(1)).onStart(span as MsrSpan) + } + @Test fun `default span status is unset`() { val span = MsrSpan.startSpan( "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ) @@ -84,6 +116,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ).setStatus(SpanStatus.Ok) @@ -96,6 +130,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ) @@ -108,6 +144,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ).end() @@ -120,6 +158,8 @@ class MsrSpanTest { "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ) @@ -129,12 +169,32 @@ class MsrSpanTest { Assert.assertEquals(1000, duration) } + @Test + fun `end triggers span processor onEnding and onEnded`() { + val span = MsrSpan.startSpan( + "span-name", + logger = logger, + timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, + idProvider = idProvider, + parentSpan = null, + ).end() as MsrSpan + + spanProcessor.inOrder { + verify().onEnding(span) + verify().onEnded(span) + } + } + @Test fun `duration is 0 for active span`() { val span = MsrSpan.startSpan( "span-name", logger = logger, timeProvider = timeProvider, + spanProcessor = spanProcessor, + sessionManager = sessionManager, idProvider = idProvider, parentSpan = null, ) diff --git a/android/measure/src/test/java/sh/measure/android/tracing/SpanStorageTest.kt b/android/measure/src/test/java/sh/measure/android/tracing/SpanStorageTest.kt index 94a8b1859..452b98168 100644 --- a/android/measure/src/test/java/sh/measure/android/tracing/SpanStorageTest.kt +++ b/android/measure/src/test/java/sh/measure/android/tracing/SpanStorageTest.kt @@ -2,7 +2,9 @@ package sh.measure.android.tracing import org.junit.Assert import org.junit.Test +import sh.measure.android.fakes.FakeSessionManager import sh.measure.android.fakes.NoopLogger +import sh.measure.android.fakes.NoopSpanProcessor import sh.measure.android.utils.AndroidTimeProvider import sh.measure.android.utils.IdProviderImpl import sh.measure.android.utils.RandomizerImpl @@ -13,6 +15,8 @@ class SpanStorageTest { private val testClock = TestClock.create() private val timeProvider = AndroidTimeProvider(testClock) private val logger = NoopLogger() + private val spanProcessor = NoopSpanProcessor() + private val sessionManager = FakeSessionManager() @Test fun `current returns null by default`() { @@ -21,7 +25,7 @@ class SpanStorageTest { @Test fun `makeCurrent sets span as current`() { - val span = MsrSpanBuilder("span-name", idProvider, timeProvider, logger).startSpan() + val span = MsrSpanBuilder("span-name", idProvider, timeProvider, spanProcessor, sessionManager, logger).startSpan() val scope = span.makeCurrent() Assert.assertEquals(span, SpanStorage.instance.current()) scope.close() @@ -29,8 +33,8 @@ class SpanStorageTest { @Test fun `closing the scope resets the current span`() { - val spanA = MsrSpanBuilder("span-A", idProvider, timeProvider, logger).startSpan() - val spanB = MsrSpanBuilder("span-B", idProvider, timeProvider, logger).startSpan() + val spanA = MsrSpanBuilder("span-A", idProvider, timeProvider, spanProcessor, sessionManager, logger).startSpan() + val spanB = MsrSpanBuilder("span-B", idProvider, timeProvider, spanProcessor, sessionManager, logger).startSpan() val spanAScope = spanA.makeCurrent() val spanBScope = spanB.makeCurrent() Assert.assertEquals(spanB, SpanStorage.instance.current())