From c6b0c3cfafec57433becbf3e1e38fed66325d848 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 27 Jan 2026 09:03:50 +0100 Subject: [PATCH] feat(distribution): Add install_groups support Add installGroupsOverride parameter to UpdateCheckParams and installGroups property to UpdateInfo for the Build Distribution SDK, matching the iOS implementation. Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 1 + .../distribution/DistributionHttpClient.kt | 4 + .../distribution/UpdateResponseParser.kt | 23 +++- .../distribution/UpdateResponseParserTest.kt | 103 +++++++++++++++++- sentry/api/sentry.api | 3 +- .../src/main/java/io/sentry/UpdateInfo.java | 12 +- 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf900983f5..606e1219ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add `installGroupsOverride` parameter and `installGroups` property to Build Distribution SDK ([#5062](https://github.com/getsentry/sentry-java/pull/5062)) - Update Android targetSdk to API 36 (Android 16) ([#5016](https://github.com/getsentry/sentry-java/pull/5016)) ### Internal diff --git a/sentry-android-distribution/src/main/java/io/sentry/android/distribution/DistributionHttpClient.kt b/sentry-android-distribution/src/main/java/io/sentry/android/distribution/DistributionHttpClient.kt index c4b2fcff8e3..ed2bbd18b1d 100644 --- a/sentry-android-distribution/src/main/java/io/sentry/android/distribution/DistributionHttpClient.kt +++ b/sentry-android-distribution/src/main/java/io/sentry/android/distribution/DistributionHttpClient.kt @@ -27,6 +27,7 @@ internal class DistributionHttpClient(private val options: SentryOptions) { val versionCode: Long, val versionName: String, val buildConfiguration: String, + val installGroupsOverride: List? = null, ) /** @@ -58,6 +59,9 @@ internal class DistributionHttpClient(private val options: SentryOptions) { append("&build_number=${URLEncoder.encode(params.versionCode.toString(), "UTF-8")}") append("&build_version=${URLEncoder.encode(params.versionName, "UTF-8")}") append("&build_configuration=${URLEncoder.encode(params.buildConfiguration, "UTF-8")}") + params.installGroupsOverride?.forEach { group -> + append("&install_groups=${URLEncoder.encode(group, "UTF-8")}") + } } val url = URL(urlString) diff --git a/sentry-android-distribution/src/main/java/io/sentry/android/distribution/UpdateResponseParser.kt b/sentry-android-distribution/src/main/java/io/sentry/android/distribution/UpdateResponseParser.kt index e97e0a5ea4c..0734b80f485 100644 --- a/sentry-android-distribution/src/main/java/io/sentry/android/distribution/UpdateResponseParser.kt +++ b/sentry-android-distribution/src/main/java/io/sentry/android/distribution/UpdateResponseParser.kt @@ -57,6 +57,7 @@ internal class UpdateResponseParser(private val options: SentryOptions) { val downloadUrl = json.optString("download_url", "") val appName = json.optString("app_name", "") val createdDate = json.optString("created_date", "") + val installGroups = parseInstallGroups(json) // Validate required fields (optString returns "null" for null values) val missingFields = mutableListOf() @@ -77,6 +78,26 @@ internal class UpdateResponseParser(private val options: SentryOptions) { ) } - return UpdateInfo(id, buildVersion, buildNumber, downloadUrl, appName, createdDate) + return UpdateInfo( + id, + buildVersion, + buildNumber, + downloadUrl, + appName, + createdDate, + installGroups, + ) + } + + private fun parseInstallGroups(json: JSONObject): List? { + val installGroupsArray = json.optJSONArray("install_groups") ?: return null + val installGroups = mutableListOf() + for (i in 0 until installGroupsArray.length()) { + val group = installGroupsArray.optString(i) + if (group.isNotEmpty() && group != "null") { + installGroups.add(group) + } + } + return if (installGroups.isEmpty()) null else installGroups } } diff --git a/sentry-android-distribution/src/test/java/io/sentry/android/distribution/UpdateResponseParserTest.kt b/sentry-android-distribution/src/test/java/io/sentry/android/distribution/UpdateResponseParserTest.kt index 473339d3b68..e15347095f6 100644 --- a/sentry-android-distribution/src/test/java/io/sentry/android/distribution/UpdateResponseParserTest.kt +++ b/sentry-android-distribution/src/test/java/io/sentry/android/distribution/UpdateResponseParserTest.kt @@ -32,7 +32,8 @@ class UpdateResponseParserTest { "build_number": 42, "download_url": "https://example.com/download", "app_name": "Test App", - "created_date": "2023-10-01T00:00:00Z" + "created_date": "2023-10-01T00:00:00Z", + "install_groups": ["beta", "internal"] }, "current": null } @@ -49,6 +50,7 @@ class UpdateResponseParserTest { assertEquals("https://example.com/download", updateInfo.downloadUrl) assertEquals("Test App", updateInfo.appName) assertEquals("2023-10-01T00:00:00Z", updateInfo.createdDate) + assertEquals(listOf("beta", "internal"), updateInfo.installGroups) } @Test @@ -355,4 +357,103 @@ class UpdateResponseParserTest { error.message.contains("Missing required fields in API response: id"), ) } + + @Test + fun `parseResponse returns null installGroups when not present`() { + val responseBody = + """ + { + "update": { + "id": "update-123", + "build_version": "2.0.0", + "build_number": 42, + "download_url": "https://example.com/download", + "app_name": "Test App", + "created_date": "2023-10-01T00:00:00Z" + } + } + """ + .trimIndent() + + val result = parser.parseResponse(200, responseBody) + + assertTrue("Should return NewRelease", result is UpdateStatus.NewRelease) + val updateInfo = (result as UpdateStatus.NewRelease).info + assertEquals(null, updateInfo.installGroups) + } + + @Test + fun `parseResponse returns null installGroups when array is empty`() { + val responseBody = + """ + { + "update": { + "id": "update-123", + "build_version": "2.0.0", + "build_number": 42, + "download_url": "https://example.com/download", + "app_name": "Test App", + "created_date": "2023-10-01T00:00:00Z", + "install_groups": [] + } + } + """ + .trimIndent() + + val result = parser.parseResponse(200, responseBody) + + assertTrue("Should return NewRelease", result is UpdateStatus.NewRelease) + val updateInfo = (result as UpdateStatus.NewRelease).info + assertEquals(null, updateInfo.installGroups) + } + + @Test + fun `parseResponse returns null installGroups when array is null`() { + val responseBody = + """ + { + "update": { + "id": "update-123", + "build_version": "2.0.0", + "build_number": 42, + "download_url": "https://example.com/download", + "app_name": "Test App", + "created_date": "2023-10-01T00:00:00Z", + "install_groups": null + } + } + """ + .trimIndent() + + val result = parser.parseResponse(200, responseBody) + + assertTrue("Should return NewRelease", result is UpdateStatus.NewRelease) + val updateInfo = (result as UpdateStatus.NewRelease).info + assertEquals(null, updateInfo.installGroups) + } + + @Test + fun `parseResponse returns single installGroup`() { + val responseBody = + """ + { + "update": { + "id": "update-123", + "build_version": "2.0.0", + "build_number": 42, + "download_url": "https://example.com/download", + "app_name": "Test App", + "created_date": "2023-10-01T00:00:00Z", + "install_groups": ["beta-testers"] + } + } + """ + .trimIndent() + + val result = parser.parseResponse(200, responseBody) + + assertTrue("Should return NewRelease", result is UpdateStatus.NewRelease) + val updateInfo = (result as UpdateStatus.NewRelease).info + assertEquals(listOf("beta-testers"), updateInfo.installGroups) + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b3e6d40be85..befd2e1adbd 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4521,13 +4521,14 @@ public class io/sentry/UncaughtExceptionHandlerIntegration$UncaughtExceptionHint } public final class io/sentry/UpdateInfo { - public fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V public fun getAppName ()Ljava/lang/String; public fun getBuildNumber ()I public fun getBuildVersion ()Ljava/lang/String; public fun getCreatedDate ()Ljava/lang/String; public fun getDownloadUrl ()Ljava/lang/String; public fun getId ()Ljava/lang/String; + public fun getInstallGroups ()Ljava/util/List; public fun toString ()Ljava/lang/String; } diff --git a/sentry/src/main/java/io/sentry/UpdateInfo.java b/sentry/src/main/java/io/sentry/UpdateInfo.java index ec9ee3b66dd..0d5df74442d 100644 --- a/sentry/src/main/java/io/sentry/UpdateInfo.java +++ b/sentry/src/main/java/io/sentry/UpdateInfo.java @@ -1,5 +1,6 @@ package io.sentry; +import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +14,7 @@ public final class UpdateInfo { private final @NotNull String downloadUrl; private final @NotNull String appName; private final @Nullable String createdDate; + private final @Nullable List installGroups; public UpdateInfo( final @NotNull String id, @@ -20,13 +22,15 @@ public UpdateInfo( final int buildNumber, final @NotNull String downloadUrl, final @NotNull String appName, - final @Nullable String createdDate) { + final @Nullable String createdDate, + final @Nullable List installGroups) { this.id = id; this.buildVersion = buildVersion; this.buildNumber = buildNumber; this.downloadUrl = downloadUrl; this.appName = appName; this.createdDate = createdDate; + this.installGroups = installGroups; } public @NotNull String getId() { @@ -53,6 +57,10 @@ public int getBuildNumber() { return createdDate; } + public @Nullable List getInstallGroups() { + return installGroups; + } + @Override public String toString() { return "UpdateInfo{" @@ -73,6 +81,8 @@ public String toString() { + ", createdDate='" + createdDate + '\'' + + ", installGroups=" + + installGroups + '}'; } }