Skip to content

Commit 4fe50fc

Browse files
feat(internal): Add serialized app start measurements getter with spans for Hybrid SDKs (#3454)
1 parent 619c9b9 commit 4fe50fc

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader {
204204
public final class io/sentry/android/core/InternalSentrySdk {
205205
public fun <init> ()V
206206
public static fun captureEnvelope ([B)Lio/sentry/protocol/SentryId;
207+
public static fun getAppStartMeasurement ()Ljava/util/Map;
207208
public static fun getCurrentScope ()Lio/sentry/IScope;
208209
public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map;
209210
}

sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.sentry.SentryLevel;
1717
import io.sentry.SentryOptions;
1818
import io.sentry.Session;
19+
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
1920
import io.sentry.android.core.performance.AppStartMetrics;
2021
import io.sentry.android.core.performance.TimeSpan;
2122
import io.sentry.protocol.App;
@@ -28,6 +29,7 @@
2829
import java.util.ArrayList;
2930
import java.util.HashMap;
3031
import java.util.List;
32+
import java.util.Locale;
3133
import java.util.Map;
3234
import java.util.concurrent.atomic.AtomicReference;
3335
import org.jetbrains.annotations.ApiStatus;
@@ -193,6 +195,63 @@ public static SentryId captureEnvelope(final @NotNull byte[] envelopeData) {
193195
return null;
194196
}
195197

198+
public static Map<String, Object> getAppStartMeasurement() {
199+
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
200+
final @NotNull List<Map<String, Object>> spans = new ArrayList<>();
201+
202+
final @NotNull TimeSpan processInitNativeSpan = new TimeSpan();
203+
processInitNativeSpan.setStartedAt(metrics.getAppStartTimeSpan().getStartUptimeMs());
204+
processInitNativeSpan.setStartUnixTimeMs(
205+
metrics.getAppStartTimeSpan().getStartTimestampMs()); // This has to go after setStartedAt
206+
processInitNativeSpan.setStoppedAt(metrics.getClassLoadedUptimeMs());
207+
processInitNativeSpan.setDescription("Process Initialization");
208+
209+
addTimeSpanToSerializedSpans(processInitNativeSpan, spans);
210+
addTimeSpanToSerializedSpans(metrics.getApplicationOnCreateTimeSpan(), spans);
211+
212+
for (final TimeSpan span : metrics.getContentProviderOnCreateTimeSpans()) {
213+
addTimeSpanToSerializedSpans(span, spans);
214+
}
215+
216+
for (final ActivityLifecycleTimeSpan span : metrics.getActivityLifecycleTimeSpans()) {
217+
addTimeSpanToSerializedSpans(span.getOnCreate(), spans);
218+
addTimeSpanToSerializedSpans(span.getOnStart(), spans);
219+
}
220+
221+
final @NotNull Map<String, Object> result = new HashMap<>();
222+
result.put("spans", spans);
223+
result.put("type", metrics.getAppStartType().toString().toLowerCase(Locale.ROOT));
224+
if (metrics.getAppStartTimeSpan().hasStarted()) {
225+
result.put("app_start_timestamp_ms", metrics.getAppStartTimeSpan().getStartTimestampMs());
226+
}
227+
228+
return result;
229+
}
230+
231+
private static void addTimeSpanToSerializedSpans(TimeSpan span, List<Map<String, Object>> spans) {
232+
if (span.hasNotStarted()) {
233+
HubAdapter.getInstance()
234+
.getOptions()
235+
.getLogger()
236+
.log(SentryLevel.WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs.");
237+
return;
238+
}
239+
240+
if (span.hasNotStopped()) {
241+
HubAdapter.getInstance()
242+
.getOptions()
243+
.getLogger()
244+
.log(SentryLevel.WARNING, "Can not convert not-stopped TimeSpan to Map for Hybrid SDKs.");
245+
return;
246+
}
247+
248+
final @NotNull Map<String, Object> spanMap = new HashMap<>();
249+
spanMap.put("description", span.getDescription());
250+
spanMap.put("start_timestamp_ms", span.getStartTimestampMs());
251+
spanMap.put("end_timestamp_ms", span.getProjectedStopTimestampMs());
252+
spans.add(spanMap);
253+
}
254+
196255
@Nullable
197256
private static Session updateSession(
198257
final @NotNull IHub hub,

sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.sentry.android.core
22

3+
import android.app.Application
4+
import android.content.ContentProvider
35
import android.content.Context
46
import androidx.test.core.app.ApplicationProvider
57
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -17,6 +19,8 @@ import io.sentry.SentryExceptionFactory
1719
import io.sentry.SentryItemType
1820
import io.sentry.SentryOptions
1921
import io.sentry.Session
22+
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan
23+
import io.sentry.android.core.performance.AppStartMetrics
2024
import io.sentry.exception.ExceptionMechanismException
2125
import io.sentry.protocol.App
2226
import io.sentry.protocol.Contexts
@@ -101,6 +105,81 @@ class InternalSentrySdkTest {
101105

102106
InternalSentrySdk.captureEnvelope(data)
103107
}
108+
109+
fun mockFinishedAppStart() {
110+
val metrics = AppStartMetrics.getInstance()
111+
112+
metrics.appStartType = AppStartMetrics.AppStartType.WARM
113+
114+
metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
115+
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
116+
metrics.appStartTimeSpan.setStoppedAt(200)
117+
metrics.classLoadedUptimeMs = 100
118+
119+
AppStartMetrics.onApplicationCreate(mock<Application>())
120+
metrics.applicationOnCreateTimeSpan.description = "Application created"
121+
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set
122+
metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite
123+
metrics.applicationOnCreateTimeSpan.setStoppedAt(40)
124+
125+
val activityLifecycleSpan = ActivityLifecycleTimeSpan()
126+
activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate"
127+
activityLifecycleSpan.onCreate.setStartedAt(50) // Can't be 0, as that's the default value if not set
128+
activityLifecycleSpan.onCreate.setStartUnixTimeMs(50) // The order matters, unix time must be set after started at in tests to avoid overwrite
129+
activityLifecycleSpan.onCreate.setStoppedAt(60)
130+
131+
activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart"
132+
activityLifecycleSpan.onStart.setStartedAt(70) // Can't be 0, as that's the default value if not set
133+
activityLifecycleSpan.onStart.setStartUnixTimeMs(70) // The order matters, unix time must be set after started at in tests to avoid overwrite
134+
activityLifecycleSpan.onStart.setStoppedAt(80)
135+
metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan)
136+
137+
AppStartMetrics.onContentProviderCreate(mock<ContentProvider>())
138+
metrics.contentProviderOnCreateTimeSpans[0].description = "Test Content Provider created"
139+
metrics.contentProviderOnCreateTimeSpans[0].setStartedAt(90)
140+
metrics.contentProviderOnCreateTimeSpans[0].setStartUnixTimeMs(90)
141+
metrics.contentProviderOnCreateTimeSpans[0].setStoppedAt(100)
142+
143+
metrics.appStartProfiler = mock()
144+
metrics.appStartSamplingDecision = mock()
145+
}
146+
147+
fun mockMinimumFinishedAppStart() {
148+
val metrics = AppStartMetrics.getInstance()
149+
150+
metrics.appStartType = AppStartMetrics.AppStartType.WARM
151+
152+
metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
153+
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
154+
metrics.appStartTimeSpan.setStoppedAt(200)
155+
metrics.classLoadedUptimeMs = 100
156+
157+
AppStartMetrics.onApplicationCreate(mock<Application>())
158+
metrics.applicationOnCreateTimeSpan.description = "Application created"
159+
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set
160+
metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite
161+
metrics.applicationOnCreateTimeSpan.setStoppedAt(40)
162+
}
163+
164+
fun mockUnfinishedAppStart() {
165+
val metrics = AppStartMetrics.getInstance()
166+
167+
metrics.appStartType = AppStartMetrics.AppStartType.WARM
168+
169+
metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set
170+
metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite
171+
metrics.appStartTimeSpan.setStoppedAt(200)
172+
metrics.classLoadedUptimeMs = 100
173+
174+
AppStartMetrics.onApplicationCreate(mock<Application>())
175+
metrics.applicationOnCreateTimeSpan.description = "Application created"
176+
metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set
177+
178+
val activityLifecycleSpan = ActivityLifecycleTimeSpan() // Expect the created spans are not started nor stopped
179+
activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate"
180+
activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart"
181+
metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan)
182+
}
104183
}
105184

106185
@BeforeTest
@@ -302,4 +381,83 @@ class InternalSentrySdkTest {
302381
}
303382
assertEquals(Session.State.Crashed, scopeRef.get().session!!.status)
304383
}
384+
385+
@Test
386+
fun `getAppStartMeasurement returns correct serialized data from the app start instance`() {
387+
Fixture().mockFinishedAppStart()
388+
389+
val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()
390+
391+
assertEquals("warm", serializedAppStart["type"])
392+
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])
393+
394+
val actualSpans = serializedAppStart["spans"] as List<*>
395+
assertEquals(5, actualSpans.size)
396+
397+
val actualProcessSpan = actualSpans[0] as Map<*, *>
398+
assertEquals("Process Initialization", actualProcessSpan["description"])
399+
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
400+
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])
401+
402+
val actualAppSpan = actualSpans[1] as Map<*, *>
403+
assertEquals("Application created", actualAppSpan["description"])
404+
assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"])
405+
assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"])
406+
407+
val actualContentProviderSpan = actualSpans[2] as Map<*, *>
408+
assertEquals("Test Content Provider created", actualContentProviderSpan["description"])
409+
assertEquals(90.toLong(), actualContentProviderSpan["start_timestamp_ms"])
410+
assertEquals(100.toLong(), actualContentProviderSpan["end_timestamp_ms"])
411+
412+
val actualActivityOnCreateSpan = actualSpans[3] as Map<*, *>
413+
assertEquals("Test Activity Lifecycle onCreate", actualActivityOnCreateSpan["description"])
414+
assertEquals(50.toLong(), actualActivityOnCreateSpan["start_timestamp_ms"])
415+
assertEquals(60.toLong(), actualActivityOnCreateSpan["end_timestamp_ms"])
416+
417+
val actualActivityOnStartSpan = actualSpans[4] as Map<*, *>
418+
assertEquals("Test Activity Lifecycle onStart", actualActivityOnStartSpan["description"])
419+
assertEquals(70.toLong(), actualActivityOnStartSpan["start_timestamp_ms"])
420+
assertEquals(80.toLong(), actualActivityOnStartSpan["end_timestamp_ms"])
421+
}
422+
423+
@Test
424+
fun `getAppStartMeasurement returns correct serialized data from the minimum app start instance`() {
425+
Fixture().mockMinimumFinishedAppStart()
426+
427+
val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()
428+
429+
assertEquals("warm", serializedAppStart["type"])
430+
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])
431+
432+
val actualSpans = serializedAppStart["spans"] as List<*>
433+
assertEquals(2, actualSpans.size)
434+
435+
val actualProcessSpan = actualSpans[0] as Map<*, *>
436+
assertEquals("Process Initialization", actualProcessSpan["description"])
437+
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
438+
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])
439+
440+
val actualAppSpan = actualSpans[1] as Map<*, *>
441+
assertEquals("Application created", actualAppSpan["description"])
442+
assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"])
443+
assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"])
444+
}
445+
446+
@Test
447+
fun `getAppStartMeasurement returns only stopped spans in serialized data`() {
448+
Fixture().mockUnfinishedAppStart()
449+
450+
val serializedAppStart = InternalSentrySdk.getAppStartMeasurement()
451+
452+
assertEquals("warm", serializedAppStart["type"])
453+
assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"])
454+
455+
val actualSpans = serializedAppStart["spans"] as List<*>
456+
assertEquals(1, actualSpans.size)
457+
458+
val actualProcessSpan = actualSpans[0] as Map<*, *>
459+
assertEquals("Process Initialization", actualProcessSpan["description"])
460+
assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"])
461+
assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"])
462+
}
305463
}

0 commit comments

Comments
 (0)