Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kw/app start span for hybrid SDKs #3454

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
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"])
}
}
Loading