diff --git a/.github/renovate.json5 b/.github/renovate.json5 index fa809ee5..bb4ce2e2 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -3,6 +3,7 @@ "extends": [ "config:base" ], + "baseBranches": ["main", "v1.7.x"], "packageRules": [ { "matchPackageNames": [ @@ -17,6 +18,15 @@ // stable version of opentelemetry-instrumentation-bom-alpha so this logic doesn't apply "ignoreUnstable": false }, + { + // Don't bump to 2.x in the 1.x line + "matchUpdateTypes": ["major"], + "matchPackagePatterns": [ + "^io.opentelemetry.instrumentation" + ], + "matchBaseBranches": ["v1.7.x"], + "enabled": false + }, { // navigation-fragment 2.7.0 and above require android api 34+, which we are not ready for // yet due to android gradle plugin only supporting min 33. diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index c50d556e..f9cb5d84 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -17,7 +17,7 @@ jobs: steps: - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@v2.4.0 + uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PAT_CLATOOL }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 70409fd3..179524c0 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -10,8 +10,8 @@ on: jobs: validation: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - - uses: gradle/actions/wrapper-validation@v4.0.0 \ No newline at end of file + - uses: gradle/actions/wrapper-validation@v4.2.1 \ No newline at end of file diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bd3b4bc7..a7e9309f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,12 +12,12 @@ concurrency: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - name: Set up JDK 17 for running Gradle - uses: actions/setup-java@v4.2.2 + uses: actions/setup-java@v4.5.0 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 39acd31c..9de91913 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -9,11 +9,11 @@ concurrency: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - name: Set up JDK 17 for running Gradle - uses: actions/setup-java@v4.2.2 + uses: actions/setup-java@v4.5.0 with: distribution: temurin java-version: 17 @@ -21,13 +21,13 @@ jobs: - name: Build and test run: touch ./local.properties; ./gradlew check javadoc assemble - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v5.0.7 check_links: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4.2.2 - name: Link Checker - uses: lycheeverse/lychee-action@v1.10.0 + uses: lycheeverse/lychee-action@v2.1.0 with: fail: true - lycheeVersion: 0.10.3 + lycheeVersion: v0.16.1 diff --git a/.lycheeignore b/.lycheeignore index bb28bb18..a7be5d7b 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -1,4 +1,5 @@ https://rum-ingest/ https://appassets.androidplatform.net/assets/index.html https://appassets.androidplatform.net/assets/first.html -https://appassets.androidplatform.net/assets/second.html \ No newline at end of file +https://appassets.androidplatform.net/assets/second.html +https://mvnrepository.com/artifact/com.splunk/splunk-otel-android/latest \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 170450cb..ca611ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Unreleased +### Version 1.7.0 - 2024-08-12 + +This is a regular maintenance release. + +This version depends on these upstream versions: + +* opentelemetry-android v0.4.0 +* opentelemetry-instrumentation-api v1.33.5 +* opentelemetry-sdk v1.41.0 + +Enhancements: + +* Disable console exporter from upstream (#894) + ### Version 1.6.0 - 2024-07-10 This is a regular maintenance release. diff --git a/README.md b/README.md index 53c4d0ba..a436f66e 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,12 @@

-

- Stable - - OpenTelemetry Instrumentation for Java Version - - - Splunk GDI specification - - - GitHub release (latest SemVer) - - - Maven Central - - - Build Status - -

+![Stable][stable-image] +[![OpenTelemetry Instrumentation for Java Version][otel-image]][otel-link] +[![OpenTelemetry Instrumentation for Android Version][android-image]][android-link] +[![Splunk GDI specification][gdi-image]][gdi-link] +[![Maven Central][maven-image]][maven-link] +[![Build Status][build-image]][build-link] --- @@ -69,3 +57,15 @@ in the official documentation. The Splunk Android RUM Instrumentation is licensed under the terms of the Apache Software License version 2.0. See [the license file](./LICENSE) for more details. + +[stable-image]: https://img.shields.io/badge/status-stable-informational?style=for-the-badge +[otel-image]: https://img.shields.io/badge/otel-1.33.5-blueviolet?style=for-the-badge +[otel-link]: https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/tag/v1.33.5 +[android-image]: https://img.shields.io/github/v/release/signalfx/splunk-otel-android?include_prereleases&style=for-the-badge +[android-link]: https://github.com/signalfx/splunk-otel-android/releases +[gdi-image]: https://img.shields.io/badge/GDI-1.4.0-blueviolet?style=for-the-badge +[gdi-link]: https://github.com/signalfx/gdi-specification/releases/tag/v1.4.0 +[maven-image]: https://img.shields.io/maven-central/v/com.splunk/splunk-otel-android?style=for-the-badge +[maven-link]: https://mvnrepository.com/artifact/com.splunk/splunk-otel-android/latest +[build-image]: https://img.shields.io/github/actions/workflow/status/signalfx/splunk-otel-android/main.yaml?branch=main&style=for-the-badge +[build-link]: https://github.com/signalfx/splunk-otel-android/actions/workflows/main.yaml diff --git a/buildSrc/src/main/kotlin/splunk.android-library-conventions.gradle.kts b/buildSrc/src/main/kotlin/splunk.android-library-conventions.gradle.kts index 7b36d021..c8fd8c4f 100644 --- a/buildSrc/src/main/kotlin/splunk.android-library-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/splunk.android-library-conventions.gradle.kts @@ -97,6 +97,7 @@ dependencies { testImplementation(libs.findLibrary("assertj-core").get()) testImplementation(libs.findBundle("mocking").get()) testImplementation(libs.findBundle("junit").get()) + testRuntimeOnly(libs.findLibrary("junit-platform-launcher").get()) testImplementation(libs.findLibrary("robolectric").get()) testImplementation(libs.findLibrary("opentelemetry-sdk-testing").get()) coreLibraryDesugaring(libs.findLibrary("desugarJdkLibs").get()) diff --git a/buildSrc/src/main/kotlin/splunk.errorprone-conventions.gradle.kts b/buildSrc/src/main/kotlin/splunk.errorprone-conventions.gradle.kts index 790151f8..c86a0d91 100644 --- a/buildSrc/src/main/kotlin/splunk.errorprone-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/splunk.errorprone-conventions.gradle.kts @@ -21,8 +21,8 @@ if (isAndroidProject) { } dependencies { - errorprone("com.uber.nullaway:nullaway:0.11.1") - errorprone("com.google.errorprone:error_prone_core:2.30.0") + errorprone("com.uber.nullaway:nullaway:0.12.1") + errorprone("com.google.errorprone:error_prone_core:2.36.0") errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") } diff --git a/gradle.properties b/gradle.properties index 6345aa72..73fdecd6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,5 +17,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true # generate the BuildConfig class that contains the app version -version=1.7.0 +version=1.8.0 group=com.splunk diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ddd78dc..e1be9eb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -opentelemetry-core = "1.41.0" -opentelemetry-core-alpha = "1.41.0-alpha" -opentelemetry-inst = "1.33.5" -opentelemetry-inst-alpha = "1.33.5-alpha" +opentelemetry-core = "1.42.1" +opentelemetry-core-alpha = "1.42.1-alpha" +opentelemetry-inst = "1.33.6" +opentelemetry-inst-alpha = "1.33.6-alpha" opentelemetry-android = "0.4.0-alpha" -mockito = "5.12.0" -junit = "5.10.3" +mockito = "5.14.2" +junit = "5.11.3" spotless = "6.25.0" -kotlin = "2.0.0" -lifecycle-runtime-ktx = "2.8.4" -activity-compose = "1.9.1" -compose-bom = "2024.06.00" +kotlin = "2.1.0" +lifecycle-runtime-ktx = "2.8.7" +activity-compose = "1.9.3" +compose-bom = "2024.11.00" navigationCompose = "2.7.7" [libraries] @@ -28,14 +28,14 @@ opentelemetry-exporter-otlp = { module = "io.opentelemetry:opentelemetry-exporte opentelemetry-exporter-logging = { module = "io.opentelemetry:opentelemetry-exporter-logging", version.ref = "opentelemetry-core" } opentelemetry-sdk-testing = { module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "opentelemetry-core" } -zipkin-sender-okhttp = "io.zipkin.reporter2:zipkin-sender-okhttp3:3.4.0" +zipkin-sender-okhttp = "io.zipkin.reporter2:zipkin-sender-okhttp3:3.4.2" androidx-browser = "androidx.browser:browser:1.8.0" -androidx-core = "androidx.core:core:1.13.1" +androidx-core = "androidx.core:core:1.15.0" androidx-navigation-fragment = "androidx.navigation:navigation-fragment:2.7.7" androidx-navigation-ui = "androidx.navigation:navigation-ui:2.7.7" -androidx-work = "androidx.work:work-runtime:2.9.1" -androidx-webkit = "androidx.webkit:webkit:1.11.0" +androidx-work = "androidx.work:work-runtime:2.10.0" +androidx-webkit = "androidx.webkit:webkit:1.12.1" # Volley android-volley = "com.android.volley:volley:1.2.1" @@ -47,16 +47,17 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "1.11.3" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit" } mockwebserver = "com.google.mockwebserver:mockwebserver:20130706" -robolectric = "org.robolectric:robolectric:4.13" +robolectric = "org.robolectric:robolectric:4.14.1" assertj-core = "org.assertj:assertj-core:3.26.3" #Compilation tools -desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.4" -android-plugin = "com.android.tools.build:gradle:8.5.2" -errorprone-plugin = "net.ltgt.gradle:gradle-errorprone-plugin:4.0.1" -nullaway-plugin = "net.ltgt.gradle:gradle-nullaway-plugin:2.0.0" +desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.1.3" +android-plugin = "com.android.tools.build:gradle:8.7.3" +errorprone-plugin = "net.ltgt.gradle:gradle-errorprone-plugin:4.1.0" +nullaway-plugin = "net.ltgt.gradle:gradle-nullaway-plugin:2.1.0" spotless-plugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c352119..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 68e8816d..eb1a55be 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts index 175a60c9..92045e3e 100644 --- a/sample-app/build.gradle.kts +++ b/sample-app/build.gradle.kts @@ -13,7 +13,7 @@ localProperties.load(FileInputStream(rootProject.file("local.properties"))) android { namespace = "com.splunk.android.sample" - compileSdk = 34 + compileSdk = 35 buildToolsVersion = "34.0.0" defaultConfig { @@ -97,6 +97,7 @@ dependencies { implementation(libs.opentelemetry.sdk) implementation(libs.opentelemetry.api.incubator) testImplementation(libs.bundles.junit) + testRuntimeOnly(libs.junit.platform.launcher) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.junit) debugImplementation(libs.androidx.ui.tooling) diff --git a/splunk-otel-android-volley/build.gradle.kts b/splunk-otel-android-volley/build.gradle.kts index a7fdf71b..a17a4445 100644 --- a/splunk-otel-android-volley/build.gradle.kts +++ b/splunk-otel-android-volley/build.gradle.kts @@ -9,7 +9,7 @@ plugins { android { namespace = "com.splunk.android.rum.volley" - compileSdk = 34 + compileSdk = 35 buildToolsVersion = "34.0.0" defaultConfig { diff --git a/splunk-otel-android-volley/src/test/java/com/splunk/rum/TracingHurlStackTest.java b/splunk-otel-android-volley/src/test/java/com/splunk/rum/TracingHurlStackTest.java index 39a8978e..43132c29 100644 --- a/splunk-otel-android-volley/src/test/java/com/splunk/rum/TracingHurlStackTest.java +++ b/splunk-otel-android-volley/src/test/java/com/splunk/rum/TracingHurlStackTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Fail.fail; +import static org.junit.Assert.assertTrue; import com.android.volley.DefaultRetryPolicy; import com.android.volley.Request; @@ -168,7 +169,10 @@ public void connectionError() throws IOException { assertThat(span.getEvents()) .hasSize(1) - .allSatisfy(e -> e.getName().equals(SemanticAttributes.EXCEPTION_EVENT_NAME)); + .allSatisfy( + e -> + assertThat(e.getName()) + .isEqualTo(SemanticAttributes.EXCEPTION_EVENT_NAME)); verifyAttributes(span, url, null, null); } @@ -191,7 +195,7 @@ public void reusedRequest() throws IOException, InterruptedException { testQueue.addToQueue(stringRequest); testQueue.addToQueue(stringRequest); - testResponseListener.countDownLatch.await(10, TimeUnit.SECONDS); + assertTrue(testResponseListener.countDownLatch.await(10, TimeUnit.SECONDS)); assertThat(server.getRequestCount()).isEqualTo(2); @@ -241,16 +245,11 @@ public void concurrency() throws IOException, InterruptedException { } latch.countDown(); - testResponseListener.countDownLatch.await(10, TimeUnit.SECONDS); + assertTrue(testResponseListener.countDownLatch.await(10, TimeUnit.SECONDS)); assertThat(server.getRequestCount()).isEqualTo(50); - otelTesting - .getSpans() - .forEach( - span -> { - verifyAttributes(span, url, 200L, "success"); - }); + otelTesting.getSpans().forEach(span -> verifyAttributes(span, url, 200L, "success")); pool.shutdown(); } diff --git a/splunk-otel-android/build.gradle.kts b/splunk-otel-android/build.gradle.kts index 38b9f0ba..9b75c676 100644 --- a/splunk-otel-android/build.gradle.kts +++ b/splunk-otel-android/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "com.splunk.android.rum" - compileSdk = 34 + compileSdk = 35 buildToolsVersion = "34.0.0" defaultConfig { diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierExtractor.java b/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierExtractor.java new file mode 100644 index 00000000..05f4f424 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierExtractor.java @@ -0,0 +1,92 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum; + +import android.app.Application; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ErrorIdentifierExtractor { + + private static final String SPLUNK_UUID_MANIFEST_KEY = "SPLUNK_O11Y_CUSTOM_UUID"; + private final Application application; + private final PackageManager packageManager; + @Nullable private final ApplicationInfo applicationInfo; + + public ErrorIdentifierExtractor(@NonNull Application application) { + this.application = application; + this.packageManager = application.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = + packageManager.getApplicationInfo( + application.getPackageName(), PackageManager.GET_META_DATA); + } catch (Exception e) { + Log.e( + SplunkRum.LOG_TAG, + "Failed to initialize ErrorIdentifierExtractor: " + e.getMessage()); + appInfo = null; + } + this.applicationInfo = appInfo; + } + + public ErrorIdentifierInfo extractInfo() { + String applicationId = null; + String versionCode = retrieveVersionCode(); + String customUUID = retrieveCustomUUID(); + + if (applicationInfo != null) { + applicationId = applicationInfo.packageName; + } else { + Log.e(SplunkRum.LOG_TAG, "ApplicationInfo is null, cannot extract applicationId"); + } + + return new ErrorIdentifierInfo(applicationId, versionCode, customUUID); + } + + @Nullable + private String retrieveVersionCode() { + try { + PackageInfo packageInfo = + packageManager.getPackageInfo(application.getPackageName(), 0); + return String.valueOf(packageInfo.versionCode); + } catch (Exception e) { + Log.e(SplunkRum.LOG_TAG, "Failed to get application version code", e); + return null; + } + } + + @Nullable + private String retrieveCustomUUID() { + if (applicationInfo == null) { + Log.e(SplunkRum.LOG_TAG, "ApplicationInfo is null; cannot retrieve Custom UUID."); + return null; + } + Bundle bundle = applicationInfo.metaData; + if (bundle != null) { + return bundle.getString(SPLUNK_UUID_MANIFEST_KEY); + } else { + Log.e(SplunkRum.LOG_TAG, "Application MetaData bundle is null"); + return null; + } + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierInfo.java b/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierInfo.java new file mode 100644 index 00000000..be6327e5 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ErrorIdentifierInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum; + +import androidx.annotation.Nullable; + +public class ErrorIdentifierInfo { + @Nullable private final String applicationId; + @Nullable private final String versionCode; + @Nullable private final String customUUID; + + public ErrorIdentifierInfo( + @Nullable String applicationId, + @Nullable String versionCode, + @Nullable String customUUID) { + this.applicationId = applicationId; + this.versionCode = versionCode; + this.customUUID = customUUID; + } + + @Nullable + public String getApplicationId() { + return applicationId; + } + + @Nullable + public String getVersionCode() { + return versionCode; + } + + @Nullable + public String getCustomUUID() { + return customUUID; + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index 3e293733..8fed2b4e 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -16,12 +16,15 @@ package com.splunk.rum; +import static com.splunk.rum.SplunkRum.APPLICATION_ID_KEY; import static com.splunk.rum.SplunkRum.APP_NAME_KEY; +import static com.splunk.rum.SplunkRum.APP_VERSION_CODE_KEY; import static com.splunk.rum.SplunkRum.COMPONENT_APPSTART; import static com.splunk.rum.SplunkRum.COMPONENT_ERROR; import static com.splunk.rum.SplunkRum.COMPONENT_KEY; import static com.splunk.rum.SplunkRum.COMPONENT_UI; import static com.splunk.rum.SplunkRum.RUM_TRACER_NAME; +import static com.splunk.rum.SplunkRum.SPLUNK_OLLY_UUID_KEY; import static io.opentelemetry.android.RumConstants.APP_START_SPAN_NAME; import static io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor.constant; import static io.opentelemetry.semconv.ResourceAttributes.DEPLOYMENT_ENVIRONMENT; @@ -40,7 +43,9 @@ import io.opentelemetry.android.config.OtelRumConfig; import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; import io.opentelemetry.android.instrumentation.anr.AnrDetector; +import io.opentelemetry.android.instrumentation.anr.AnrDetectorBuilder; import io.opentelemetry.android.instrumentation.crash.CrashReporter; +import io.opentelemetry.android.instrumentation.crash.CrashReporterBuilder; import io.opentelemetry.android.instrumentation.lifecycle.AndroidLifecycleInstrumentation; import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; import io.opentelemetry.android.instrumentation.slowrendering.SlowRenderingDetector; @@ -286,45 +291,70 @@ private Resource createSplunkResource() { private void installAnrDetector(OpenTelemetryRumBuilder otelRumBuilder, Looper mainLooper) { otelRumBuilder.addInstrumentation( instrumentedApplication -> { - AnrDetector.builder() - .addAttributesExtractor(constant(COMPONENT_KEY, COMPONENT_ERROR)) - .setMainLooper(mainLooper) - .build() - .installOn(instrumentedApplication); + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(application); + ErrorIdentifierInfo errorIdentifierInfo = extractor.extractInfo(); + String applicationId = errorIdentifierInfo.getApplicationId(); + String versionCode = errorIdentifierInfo.getVersionCode(); + String uuid = errorIdentifierInfo.getCustomUUID(); - initializationEvents.emit("anrMonitorInitialized"); - }); - } + AnrDetectorBuilder builder = AnrDetector.builder(); + builder.addAttributesExtractor(constant(COMPONENT_KEY, COMPONENT_ERROR)); - private void installSlowRenderingDetector(OpenTelemetryRumBuilder otelRumBuilder) { - otelRumBuilder.addInstrumentation( - instrumentedApplication -> { - SlowRenderingDetector.builder() - .setSlowRenderingDetectionPollInterval( - builder.slowRenderingDetectionPollInterval) - .build() - .installOn(instrumentedApplication); - initializationEvents.emit("slowRenderingDetectorInitialized"); + if (applicationId != null) + builder.addAttributesExtractor(constant(APPLICATION_ID_KEY, applicationId)); + if (versionCode != null) + builder.addAttributesExtractor(constant(APP_VERSION_CODE_KEY, versionCode)); + if (uuid != null) + builder.addAttributesExtractor(constant(SPLUNK_OLLY_UUID_KEY, uuid)); + + builder.setMainLooper(mainLooper).build().installOn(instrumentedApplication); + + initializationEvents.emit("anrMonitorInitialized"); }); } private void installCrashReporter(OpenTelemetryRumBuilder otelRumBuilder) { otelRumBuilder.addInstrumentation( instrumentedApplication -> { - CrashReporter.builder() - .addAttributesExtractor( + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(application); + ErrorIdentifierInfo errorIdentifierInfo = extractor.extractInfo(); + String applicationId = errorIdentifierInfo.getApplicationId(); + String versionCode = errorIdentifierInfo.getVersionCode(); + String uuid = errorIdentifierInfo.getCustomUUID(); + + CrashReporterBuilder builder = CrashReporter.builder(); + builder.addAttributesExtractor( RuntimeDetailsExtractor.create( instrumentedApplication .getApplication() .getApplicationContext())) - .addAttributesExtractor(new CrashComponentExtractor()) - .build() - .installOn(instrumentedApplication); + .addAttributesExtractor(new CrashComponentExtractor()); + + if (applicationId != null) + builder.addAttributesExtractor(constant(APPLICATION_ID_KEY, applicationId)); + if (versionCode != null) + builder.addAttributesExtractor(constant(APP_VERSION_CODE_KEY, versionCode)); + if (uuid != null) + builder.addAttributesExtractor(constant(SPLUNK_OLLY_UUID_KEY, uuid)); + + builder.build().installOn(instrumentedApplication); initializationEvents.emit("crashReportingInitialized"); }); } + private void installSlowRenderingDetector(OpenTelemetryRumBuilder otelRumBuilder) { + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + SlowRenderingDetector.builder() + .setSlowRenderingDetectionPollInterval( + builder.slowRenderingDetectionPollInterval) + .build() + .installOn(instrumentedApplication); + initializationEvents.emit("slowRenderingDetectorInitialized"); + }); + } + // visible for testing SpanExporter buildFilteringExporter( CurrentNetworkProvider currentNetworkProvider, diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumResponseAttributesExtractor.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumResponseAttributesExtractor.java index 7f8cdef4..3b116c72 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumResponseAttributesExtractor.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumResponseAttributesExtractor.java @@ -28,6 +28,7 @@ class RumResponseAttributesExtractor implements AttributesExtractor { + public static final String SERVER_TIMING_HEADER = "server-timing"; private final ServerTimingHeaderParser serverTimingHeaderParser; public RumResponseAttributesExtractor(ServerTimingHeaderParser serverTimingHeaderParser) { @@ -52,11 +53,18 @@ public void onEnd( } private void onResponse(AttributesBuilder attributes, Response response) { - String serverTimingHeader = response.header("Server-Timing"); - String[] ids = serverTimingHeaderParser.parse(serverTimingHeader); - if (ids.length == 2) { - attributes.put(LINK_TRACE_ID_KEY, ids[0]); - attributes.put(LINK_SPAN_ID_KEY, ids[1]); - } + response.headers() + .forEach( + header -> { + if (!header.getFirst().equalsIgnoreCase(SERVER_TIMING_HEADER)) { + return; + } + + String[] ids = serverTimingHeaderParser.parse(header.getSecond()); + if (ids.length == 2) { + attributes.put(LINK_TRACE_ID_KEY, ids[0]); + attributes.put(LINK_SPAN_ID_KEY, ids[1]); + } + }); } } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java index 33551378..63d85b33 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java @@ -67,6 +67,9 @@ public class SplunkRum { static final AttributeKey APP_NAME_KEY = stringKey("app"); static final AttributeKey RUM_VERSION_KEY = stringKey("splunk.rum.version"); + static final AttributeKey APPLICATION_ID_KEY = stringKey("service.application_id"); + static final AttributeKey APP_VERSION_CODE_KEY = stringKey("service.version_code"); + static final AttributeKey SPLUNK_OLLY_UUID_KEY = stringKey("service.o11y.key"); @Nullable private static SplunkRum INSTANCE; diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/ErrorIdentifierExtractorTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/ErrorIdentifierExtractorTest.java new file mode 100644 index 00000000..0bdee7eb --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/ErrorIdentifierExtractorTest.java @@ -0,0 +1,122 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import android.app.Application; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ErrorIdentifierExtractorTest { + private static final String SPLUNK_UUID_MANIFEST_KEY = "SPLUNK_O11Y_CUSTOM_UUID"; + private static final String TEST_PACKAGE_NAME = "splunk.test.package.name"; + private static final String TEST_VERSION_CODE = "123"; + private static final String TEST_UUID = "test-uuid"; + + @Mock private Application mockApplication; + @Mock private PackageManager mockPackageManager; + @Mock private PackageInfo mockPackageInfo; + @Mock private ApplicationInfo mockApplicationInfo; + @Mock private Bundle mockMetadata; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + when(mockApplication.getApplicationContext()).thenReturn(mockApplication); + when(mockApplication.getPackageManager()).thenReturn(mockPackageManager); + when(mockApplication.getPackageName()).thenReturn(TEST_PACKAGE_NAME); + + mockApplicationInfo.packageName = TEST_PACKAGE_NAME; + mockApplicationInfo.metaData = mockMetadata; + + when(mockPackageManager.getApplicationInfo(TEST_PACKAGE_NAME, PackageManager.GET_META_DATA)) + .thenReturn(mockApplicationInfo); + when(mockMetadata.getString(SPLUNK_UUID_MANIFEST_KEY)).thenReturn(TEST_UUID); + + mockPackageInfo.versionCode = 123; + when(mockPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mockPackageInfo); + } + + @Test + public void testGetApplicationId() { + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertEquals(TEST_PACKAGE_NAME, extractor.extractInfo().getApplicationId()); + } + + @Test + public void testGetVersionCode() { + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertEquals(TEST_VERSION_CODE, extractor.extractInfo().getVersionCode()); + } + + @Test + public void testGetCustomUUID() { + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertEquals(TEST_UUID, extractor.extractInfo().getCustomUUID()); + } + + @Test + public void testCustomUUIDButDoesNotExist() { + when(mockMetadata.getString(SPLUNK_UUID_MANIFEST_KEY)).thenReturn(null); + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertNull(extractor.extractInfo().getCustomUUID()); + } + + @Test + public void testApplicationInfoMetaDataIsNull() throws PackageManager.NameNotFoundException { + ApplicationInfo applicationInfoWithNullMetaData = new ApplicationInfo(); + applicationInfoWithNullMetaData.packageName = TEST_PACKAGE_NAME; + + when(mockPackageManager.getApplicationInfo(TEST_PACKAGE_NAME, PackageManager.GET_META_DATA)) + .thenReturn(applicationInfoWithNullMetaData); + + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertNull(extractor.extractInfo().getCustomUUID()); + } + + @Test + public void testRetrieveVersionCodeIsNull() throws PackageManager.NameNotFoundException { + when(mockPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)) + .thenThrow(new PackageManager.NameNotFoundException()); + + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + assertNull(extractor.extractInfo().getVersionCode()); + } + + @Test + public void testExtractInfoWhenApplicationInfoIsNull() + throws PackageManager.NameNotFoundException { + when(mockPackageManager.getApplicationInfo(TEST_PACKAGE_NAME, PackageManager.GET_META_DATA)) + .thenThrow(new PackageManager.NameNotFoundException()); + + ErrorIdentifierExtractor extractor = new ErrorIdentifierExtractor(mockApplication); + + ErrorIdentifierInfo info = extractor.extractInfo(); + assertNull(info.getApplicationId()); + assertEquals(TEST_VERSION_CODE, info.getVersionCode()); + assertNull(info.getCustomUUID()); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumResponseAttributesExtractorTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumResponseAttributesExtractorTest.java index 1ca693b2..7aae9061 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumResponseAttributesExtractorTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumResponseAttributesExtractorTest.java @@ -36,32 +36,64 @@ class RumResponseAttributesExtractorTest { @Test void spanDecoration() { - ServerTimingHeaderParser headerParser = mock(ServerTimingHeaderParser.class); - when(headerParser.parse("headerValue")) - .thenReturn(new String[] {"9499195c502eb217c448a68bfe0f967c", "fe16eca542cd5d86"}); + Request fakeRequest = mock(Request.class); + Response response = + getBaseRuestBuilder(fakeRequest) + .addHeader( + "Server-Timing", + "traceparent;desc=\"00-00000000000000000000000000000001-0000000000000001-01\"") + .build(); + Attributes attributes = performAttributesExtraction(fakeRequest, response); + + assertThat(attributes) + .containsOnly( + entry(COMPONENT_KEY, "http"), + entry(LINK_TRACE_ID_KEY, "00000000000000000000000000000001"), + entry(LINK_SPAN_ID_KEY, "0000000000000001")); + } + @Test + void ignoresMalformed() { Request fakeRequest = mock(Request.class); Response response = - new Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_1_1) - .message("hello") - .code(200) - .addHeader("Server-Timing", "headerValue") + getBaseRuestBuilder(fakeRequest) + .addHeader("Server-Timing", "othervalue 1") + .addHeader( + "Server-Timing", + "traceparent;desc=\"00-00000000000000000000000000000001-0000000000000001-01\"") + .addHeader("Server-Timing", "othervalue 2") .build(); + Attributes attributes = performAttributesExtraction(fakeRequest, response); - RumResponseAttributesExtractor attributesExtractor = - new RumResponseAttributesExtractor(headerParser); - AttributesBuilder attributesBuilder = Attributes.builder(); - attributesExtractor.onStart(attributesBuilder, Context.root(), fakeRequest); - attributesExtractor.onEnd(attributesBuilder, Context.root(), fakeRequest, response, null); - Attributes attributes = attributesBuilder.build(); + assertThat(attributes) + .containsOnly( + entry(COMPONENT_KEY, "http"), + entry(LINK_TRACE_ID_KEY, "00000000000000000000000000000001"), + entry(LINK_SPAN_ID_KEY, "0000000000000001")); + } + + @Test + void lastMatchingWins() { + Request fakeRequest = mock(Request.class); + Response response = + getBaseRuestBuilder(fakeRequest) + .addHeader( + "Server-Timing", + "traceparent;desc=\"00-00000000000000000000000000000001-0000000000000001-01\"") + .addHeader( + "Server-Timing", + "traceparent;desc=\"00-00000000000000000000000000000002-0000000000000002-01\"") + .addHeader( + "Server-Timing", + "traceparent;desc=\"00-00000000000000000000000000000003-0000000000000003-01\"") + .build(); + Attributes attributes = performAttributesExtraction(fakeRequest, response); assertThat(attributes) .containsOnly( entry(COMPONENT_KEY, "http"), - entry(LINK_TRACE_ID_KEY, "9499195c502eb217c448a68bfe0f967c"), - entry(LINK_SPAN_ID_KEY, "fe16eca542cd5d86")); + entry(LINK_TRACE_ID_KEY, "00000000000000000000000000000003"), + entry(LINK_SPAN_ID_KEY, "0000000000000003")); } @Test @@ -70,21 +102,27 @@ void spanDecoration_noLinkingHeader() { when(headerParser.parse(null)).thenReturn(new String[0]); Request fakeRequest = mock(Request.class); - Response response = - new Response.Builder() - .request(fakeRequest) - .protocol(Protocol.HTTP_1_1) - .message("hello") - .code(200) - .build(); + Response response = getBaseRuestBuilder(fakeRequest).build(); + Attributes attributes = performAttributesExtraction(fakeRequest, response); + + assertThat(attributes).containsOnly(entry(COMPONENT_KEY, "http")); + } + private static Attributes performAttributesExtraction(Request fakeRequest, Response response) { RumResponseAttributesExtractor attributesExtractor = - new RumResponseAttributesExtractor(headerParser); + new RumResponseAttributesExtractor(new ServerTimingHeaderParser()); AttributesBuilder attributesBuilder = Attributes.builder(); - attributesExtractor.onEnd(attributesBuilder, Context.root(), fakeRequest, response, null); attributesExtractor.onStart(attributesBuilder, Context.root(), fakeRequest); + attributesExtractor.onEnd(attributesBuilder, Context.root(), fakeRequest, response, null); Attributes attributes = attributesBuilder.build(); + return attributes; + } - assertThat(attributes).containsOnly(entry(COMPONENT_KEY, "http")); + private Response.Builder getBaseRuestBuilder(Request fakeRequest) { + return new Response.Builder() + .request(fakeRequest) + .protocol(Protocol.HTTP_1_1) + .message("hello") + .code(200); } }