Skip to content

Commit 38b4a84

Browse files
feat: O11Y-601 - Add Android launch time instrumentation (#274)
## Summary This commit introduces automatic instrumentation for Android application launch times. It measures and reports two key metrics: - Time to Initial Display (TTID) - Time to Full Display (TTFD) The instrumentation classifies launches as `cold`, `warm`, or `hot` and records the durations as histogram metrics (`app.launch.duration.ttid` and `app.launch.duration.ttfd`). These metrics include attributes for the launch type and the specific activity being launched. The implementation also includes: - Adding the dependency `androidx.activity:activity`. - Updating the Android compile SDK version to 36 (Required for the dependency added) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Android launch-time metrics (TTID, TTFD), integrates instrumentation into the SDK, switches metrics export to delta temporality, and updates compileSdk to 36 with new metric APIs and demo triggers. > > - **SDK**: > - **Launch time instrumentation**: Add `LaunchTimeInstrumentation` to record `app.launch.duration.ttid` and `app.launch.duration.ttfd` with `launch.type` and `launch.activity` attributes. > - **Integration**: Wire instrumentation into `InstrumentationManager` (enabled when metrics are on). > - **Metrics export**: Use delta temporality via `AggregationTemporalitySelector.deltaPreferred()` for OTLP and debug exporters; adjust periodic reader interval to milliseconds. > - **Dependencies/Config**: Add `androidx.activity:activity`; bump `compileSdk` to `36`. > - **E2E demo app**: > - Add UI buttons to trigger new metric types; update `ViewModel` with `recordHistogram`, `recordCount`, `recordIncr`, `recordUpDownCounter` and rename gauge metric to `test-gauge`. > - Bump `compileSdk` to `36`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e5c261d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Todd Anderson <[email protected]>
1 parent 9fb304d commit 38b4a84

File tree

7 files changed

+525
-11
lines changed

7 files changed

+525
-11
lines changed

e2e/android/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77

88
android {
99
namespace = "com.example.androidobservability"
10-
compileSdk = 35
10+
compileSdk = 36
1111

1212
defaultConfig {
1313
applicationId = "com.example.androidobservability"

e2e/android/app/src/main/java/com/example/androidobservability/MainActivity.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
1010
import androidx.compose.foundation.layout.Spacer
1111
import androidx.compose.foundation.layout.fillMaxSize
1212
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.imePadding
1314
import androidx.compose.foundation.layout.padding
1415
import androidx.compose.foundation.rememberScrollState
1516
import androidx.compose.foundation.verticalScroll
@@ -35,16 +36,21 @@ class MainActivity : ComponentActivity() {
3536
enableEdgeToEdge()
3637
setContent {
3738
AndroidObservabilityTheme {
38-
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
39+
Scaffold(
40+
modifier = Modifier
41+
.fillMaxSize()
42+
.imePadding()
43+
) { innerPadding ->
3944
var customLogText by remember { mutableStateOf("") }
4045
var customSpanText by remember { mutableStateOf("") }
4146
var customContextKey by remember { mutableStateOf("") }
4247

4348
Column(
4449
modifier = Modifier
50+
.fillMaxSize()
4551
.padding(innerPadding)
46-
.padding(16.dp)
4752
.verticalScroll(rememberScrollState())
53+
.padding(16.dp)
4854
) {
4955
Text(
5056
text = "Hello Telemetry",
@@ -77,6 +83,34 @@ class MainActivity : ComponentActivity() {
7783
) {
7884
Text("Trigger Metric")
7985
}
86+
Button(
87+
onClick = {
88+
viewModel.triggerHistogramMetric()
89+
}
90+
) {
91+
Text("Trigger Histogram Metric")
92+
}
93+
Button(
94+
onClick = {
95+
viewModel.triggerCountMetric()
96+
}
97+
) {
98+
Text("Trigger Count Metric")
99+
}
100+
Button(
101+
onClick = {
102+
viewModel.triggerIncrementalMetric()
103+
}
104+
) {
105+
Text("Trigger Incremental Metric")
106+
}
107+
Button(
108+
onClick = {
109+
viewModel.triggerUpDownCounterMetric()
110+
}
111+
) {
112+
Text("Trigger UpDownCounter Metric")
113+
}
80114
Button(
81115
onClick = {
82116
viewModel.triggerError()

e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,23 @@ import java.net.URL
2121
class ViewModel : ViewModel() {
2222

2323
fun triggerMetric() {
24-
LDObserve.recordMetric(Metric("test", 50.0))
24+
LDObserve.recordMetric(Metric("test-gauge", 50.0))
25+
}
26+
27+
fun triggerHistogramMetric() {
28+
LDObserve.recordHistogram(Metric("test-histogram", 15.0))
29+
}
30+
31+
fun triggerCountMetric() {
32+
LDObserve.recordCount(Metric("test-counter", 10.0))
33+
}
34+
35+
fun triggerIncrementalMetric() {
36+
LDObserve.recordIncr(Metric("test-incremental-counter", 12.0))
37+
}
38+
39+
fun triggerUpDownCounterMetric() {
40+
LDObserve.recordUpDownCounter(Metric("test-up-down-counter", 25.0))
2541
}
2642

2743
fun triggerError() {

sdk/@launchdarkly/observability-android/lib/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ dependencies {
2323
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
2424
implementation("com.jakewharton.timber:timber:5.0.1")
2525

26+
// Android
27+
implementation("androidx.activity:activity:1.11.0")
28+
2629
// Coroutines
2730
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
2831
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
@@ -69,7 +72,7 @@ tasks.withType<Test> {
6972

7073
android {
7174
namespace = "com.launchdarkly.observability"
72-
compileSdk = 35
75+
compileSdk = 36
7376

7477
buildFeatures {
7578
buildConfig = true

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/DebugMetricExporter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import io.opentelemetry.sdk.common.CompletableResultCode
55
import io.opentelemetry.sdk.metrics.InstrumentType
66
import io.opentelemetry.sdk.metrics.data.AggregationTemporality
77
import io.opentelemetry.sdk.metrics.data.MetricData
8+
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector
89
import io.opentelemetry.sdk.metrics.export.MetricExporter
910

1011
class DebugMetricExporter(private val logger: LDLogger): MetricExporter {
1112

1213
override fun export(metrics: Collection<MetricData?>): CompletableResultCode? {
1314
for (metric in metrics) {
14-
logger.info(metric.toString())
15+
logger.info("Metric exported: ${metric.toString()}")
1516
}
1617
return CompletableResultCode.ofSuccess()
1718
}
1819

1920
override fun flush(): CompletableResultCode = CompletableResultCode.ofSuccess()
2021
override fun shutdown(): CompletableResultCode = CompletableResultCode.ofSuccess()
21-
override fun getAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality? = AggregationTemporality.CUMULATIVE
22+
override fun getAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality? =
23+
AggregationTemporalitySelector.deltaPreferred().getAggregationTemporality(instrumentType)
2224
}

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder
3333
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor
3434
import io.opentelemetry.sdk.logs.export.LogRecordExporter
3535
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder
36+
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector
3637
import io.opentelemetry.sdk.metrics.export.MetricExporter
3738
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader
3839
import io.opentelemetry.sdk.resources.Resource
@@ -75,7 +76,7 @@ class InstrumentationManager(
7576
private const val BATCH_SCHEDULE_DELAY_MS = 1000L
7677
private const val BATCH_EXPORTER_TIMEOUT_MS = 5000L
7778
private const val BATCH_MAX_EXPORT_SIZE = 10
78-
private const val METRICS_EXPORT_INTERVAL_SECONDS = 10L
79+
private const val METRICS_EXPORT_INTERVAL_MS = 10_000L
7980
private const val FLUSH_TIMEOUT_SECONDS = 5L
8081
}
8182

@@ -93,6 +94,7 @@ class InstrumentationManager(
9394
private var spanProcessor: BatchSpanProcessor? = null
9495
private var logProcessor: BatchLogRecordProcessor? = null
9596
private var metricsReader: PeriodicMetricReader? = null
97+
private var launchTimeInstrumentation: LaunchTimeInstrumentation? = null
9698
private val gaugeCache = ConcurrentHashMap<String, DoubleGauge>()
9799
private val counterCache = ConcurrentHashMap<String, LongCounter>()
98100
private val histogramCache = ConcurrentHashMap<String, DoubleHistogram>()
@@ -104,7 +106,7 @@ class InstrumentationManager(
104106
init {
105107
val otelRumConfig = createOtelRumConfig()
106108

107-
otelRUM = OpenTelemetryRum.builder(application, otelRumConfig)
109+
val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig)
108110
.addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ ->
109111
return@addLoggerProviderCustomizer if (options.disableLogs && options.disableErrorTracking) {
110112
sdkLoggerProviderBuilder
@@ -126,7 +128,17 @@ class InstrumentationManager(
126128
configureMeterProvider(sdkMeterProviderBuilder)
127129
}
128130
}
129-
.build()
131+
132+
if (!options.disableMetrics) {
133+
launchTimeInstrumentation = LaunchTimeInstrumentation(
134+
application = application,
135+
metricRecorder = this::recordHistogram
136+
).also {
137+
rumBuilder.addInstrumentation(it)
138+
}
139+
}
140+
141+
otelRUM = rumBuilder.build()
130142

131143
initializeTelemetryInspector()
132144
loadSamplingConfigAsync()
@@ -211,6 +223,7 @@ class InstrumentationManager(
211223
return OtlpHttpMetricExporter.builder()
212224
.setEndpoint(options.otlpEndpoint + METRICS_PATH)
213225
.setHeaders { options.customHeaders }
226+
.setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred())
214227
.build()
215228
}
216229

@@ -275,7 +288,7 @@ class InstrumentationManager(
275288
private fun createPeriodicMetricReader(metricExporter: MetricExporter): PeriodicMetricReader {
276289
// Configure a periodic reader that pushes metrics every 10 seconds.
277290
return PeriodicMetricReader.builder(metricExporter)
278-
.setInterval(METRICS_EXPORT_INTERVAL_SECONDS, TimeUnit.SECONDS)
291+
.setInterval(METRICS_EXPORT_INTERVAL_MS, TimeUnit.MILLISECONDS)
279292
.build()
280293
}
281294

@@ -332,6 +345,7 @@ class InstrumentationManager(
332345
val counter = counterCache.getOrPut(metric.name) {
333346
otelMeter.counterBuilder(metric.name).build()
334347
}
348+
// It increments the value until the metric is exported, then it’s reset.
335349
counter.add(1, metric.attributes)
336350
}
337351

0 commit comments

Comments
 (0)