Skip to content

Commit

Permalink
feat(internal): Add serialized app start measurements getter with spa…
Browse files Browse the repository at this point in the history
…ns for Hybrid SDKs (#3454)
  • Loading branch information
krystofwoldrich committed Jun 4, 2024
1 parent 619c9b9 commit 4fe50fc
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader {
public final class io/sentry/android/core/InternalSentrySdk {
public fun <init> ()V
public static fun captureEnvelope ([B)Lio/sentry/protocol/SentryId;
public static fun getAppStartMeasurement ()Ljava/util/Map;
public static fun getCurrentScope ()Lio/sentry/IScope;
public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.Session;
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.App;
Expand All @@ -28,6 +29,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.annotations.ApiStatus;
Expand Down Expand Up @@ -193,6 +195,63 @@ public static SentryId captureEnvelope(final @NotNull byte[] envelopeData) {
return null;
}

public static Map<String, Object> getAppStartMeasurement() {
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
final @NotNull List<Map<String, Object>> spans = new ArrayList<>();

final @NotNull TimeSpan processInitNativeSpan = new TimeSpan();
processInitNativeSpan.setStartedAt(metrics.getAppStartTimeSpan().getStartUptimeMs());
processInitNativeSpan.setStartUnixTimeMs(
metrics.getAppStartTimeSpan().getStartTimestampMs()); // This has to go after setStartedAt
processInitNativeSpan.setStoppedAt(metrics.getClassLoadedUptimeMs());
processInitNativeSpan.setDescription("Process Initialization");

addTimeSpanToSerializedSpans(processInitNativeSpan, spans);
addTimeSpanToSerializedSpans(metrics.getApplicationOnCreateTimeSpan(), spans);

for (final TimeSpan span : metrics.getContentProviderOnCreateTimeSpans()) {
addTimeSpanToSerializedSpans(span, spans);
}

for (final ActivityLifecycleTimeSpan span : metrics.getActivityLifecycleTimeSpans()) {
addTimeSpanToSerializedSpans(span.getOnCreate(), spans);
addTimeSpanToSerializedSpans(span.getOnStart(), spans);
}

final @NotNull Map<String, Object> result = new HashMap<>();
result.put("spans", spans);
result.put("type", metrics.getAppStartType().toString().toLowerCase(Locale.ROOT));
if (metrics.getAppStartTimeSpan().hasStarted()) {
result.put("app_start_timestamp_ms", metrics.getAppStartTimeSpan().getStartTimestampMs());
}

return result;
}

private static void addTimeSpanToSerializedSpans(TimeSpan span, List<Map<String, Object>> spans) {
if (span.hasNotStarted()) {
HubAdapter.getInstance()
.getOptions()
.getLogger()
.log(SentryLevel.WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs.");
return;
}

if (span.hasNotStopped()) {
HubAdapter.getInstance()
.getOptions()
.getLogger()
.log(SentryLevel.WARNING, "Can not convert not-stopped TimeSpan to Map for Hybrid SDKs.");
return;
}

final @NotNull Map<String, Object> spanMap = new HashMap<>();
spanMap.put("description", span.getDescription());
spanMap.put("start_timestamp_ms", span.getStartTimestampMs());
spanMap.put("end_timestamp_ms", span.getProjectedStopTimestampMs());
spans.add(spanMap);
}

@Nullable
private static Session updateSession(
final @NotNull IHub hub,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.sentry.android.core

import android.app.Application
import android.content.ContentProvider
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand All @@ -17,6 +19,8 @@ import io.sentry.SentryExceptionFactory
import io.sentry.SentryItemType
import io.sentry.SentryOptions
import io.sentry.Session
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan
import io.sentry.android.core.performance.AppStartMetrics
import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.App
import io.sentry.protocol.Contexts
Expand Down Expand Up @@ -101,6 +105,81 @@ class InternalSentrySdkTest {

InternalSentrySdk.captureEnvelope(data)
}

fun mockFinishedAppStart() {
val metrics = AppStartMetrics.getInstance()

metrics.appStartType = AppStartMetrics.AppStartType.WARM

metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
metrics.appStartTimeSpan.setStoppedAt(200)
metrics.classLoadedUptimeMs = 100

AppStartMetrics.onApplicationCreate(mock<Application>())
metrics.applicationOnCreateTimeSpan.description = "Application created"
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set
metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite
metrics.applicationOnCreateTimeSpan.setStoppedAt(40)

val activityLifecycleSpan = ActivityLifecycleTimeSpan()
activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate"
activityLifecycleSpan.onCreate.setStartedAt(50) // Can't be 0, as that's the default value if not set
activityLifecycleSpan.onCreate.setStartUnixTimeMs(50) // The order matters, unix time must be set after started at in tests to avoid overwrite
activityLifecycleSpan.onCreate.setStoppedAt(60)

activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart"
activityLifecycleSpan.onStart.setStartedAt(70) // Can't be 0, as that's the default value if not set
activityLifecycleSpan.onStart.setStartUnixTimeMs(70) // The order matters, unix time must be set after started at in tests to avoid overwrite
activityLifecycleSpan.onStart.setStoppedAt(80)
metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan)

AppStartMetrics.onContentProviderCreate(mock<ContentProvider>())
metrics.contentProviderOnCreateTimeSpans[0].description = "Test Content Provider created"
metrics.contentProviderOnCreateTimeSpans[0].setStartedAt(90)
metrics.contentProviderOnCreateTimeSpans[0].setStartUnixTimeMs(90)
metrics.contentProviderOnCreateTimeSpans[0].setStoppedAt(100)

metrics.appStartProfiler = mock()
metrics.appStartSamplingDecision = mock()
}

fun mockMinimumFinishedAppStart() {
val metrics = AppStartMetrics.getInstance()

metrics.appStartType = AppStartMetrics.AppStartType.WARM

metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
metrics.appStartTimeSpan.setStoppedAt(200)
metrics.classLoadedUptimeMs = 100

AppStartMetrics.onApplicationCreate(mock<Application>())
metrics.applicationOnCreateTimeSpan.description = "Application created"
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set
metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite
metrics.applicationOnCreateTimeSpan.setStoppedAt(40)
}

fun mockUnfinishedAppStart() {
val metrics = AppStartMetrics.getInstance()

metrics.appStartType = AppStartMetrics.AppStartType.WARM

metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
metrics.appStartTimeSpan.setStoppedAt(200)
metrics.classLoadedUptimeMs = 100

AppStartMetrics.onApplicationCreate(mock<Application>())
metrics.applicationOnCreateTimeSpan.description = "Application created"
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set

val activityLifecycleSpan = ActivityLifecycleTimeSpan() // Expect the created spans are not started nor stopped
activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate"
activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart"
metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan)
}
}

@BeforeTest
Expand Down Expand Up @@ -302,4 +381,83 @@ class InternalSentrySdkTest {
}
assertEquals(Session.State.Crashed, scopeRef.get().session!!.status)
}

@Test
fun `getAppStartMeasurement returns correct serialized data from the app start instance`() {
Fixture().mockFinishedAppStart()

val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()

assertEquals("warm", serializedAppStart["type"])
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])

val actualSpans = serializedAppStart["spans"] as List<*>
assertEquals(5, actualSpans.size)

val actualProcessSpan = actualSpans[0] as Map<*, *>
assertEquals("Process Initialization", actualProcessSpan["description"])
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])

val actualAppSpan = actualSpans[1] as Map<*, *>
assertEquals("Application created", actualAppSpan["description"])
assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"])
assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"])

val actualContentProviderSpan = actualSpans[2] as Map<*, *>
assertEquals("Test Content Provider created", actualContentProviderSpan["description"])
assertEquals(90.toLong(), actualContentProviderSpan["start_timestamp_ms"])
assertEquals(100.toLong(), actualContentProviderSpan["end_timestamp_ms"])

val actualActivityOnCreateSpan = actualSpans[3] as Map<*, *>
assertEquals("Test Activity Lifecycle onCreate", actualActivityOnCreateSpan["description"])
assertEquals(50.toLong(), actualActivityOnCreateSpan["start_timestamp_ms"])
assertEquals(60.toLong(), actualActivityOnCreateSpan["end_timestamp_ms"])

val actualActivityOnStartSpan = actualSpans[4] as Map<*, *>
assertEquals("Test Activity Lifecycle onStart", actualActivityOnStartSpan["description"])
assertEquals(70.toLong(), actualActivityOnStartSpan["start_timestamp_ms"])
assertEquals(80.toLong(), actualActivityOnStartSpan["end_timestamp_ms"])
}

@Test
fun `getAppStartMeasurement returns correct serialized data from the minimum app start instance`() {
Fixture().mockMinimumFinishedAppStart()

val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()

assertEquals("warm", serializedAppStart["type"])
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])

val actualSpans = serializedAppStart["spans"] as List<*>
assertEquals(2, actualSpans.size)

val actualProcessSpan = actualSpans[0] as Map<*, *>
assertEquals("Process Initialization", actualProcessSpan["description"])
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])

val actualAppSpan = actualSpans[1] as Map<*, *>
assertEquals("Application created", actualAppSpan["description"])
assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"])
assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"])
}

@Test
fun `getAppStartMeasurement returns only stopped spans in serialized data`() {
Fixture().mockUnfinishedAppStart()

val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()

assertEquals("warm", serializedAppStart["type"])
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])

val actualSpans = serializedAppStart["spans"] as List<*>
assertEquals(1, actualSpans.size)

val actualProcessSpan = actualSpans[0] as Map<*, *>
assertEquals("Process Initialization", actualProcessSpan["description"])
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])
}
}

0 comments on commit 4fe50fc

Please sign in to comment.