Skip to content

Commit adca534

Browse files
authored
Merge branch 'main' into feat/use-modifier-param-SentryTraced
2 parents 302eacf + 4c657b6 commit adca534

File tree

10 files changed

+670
-6
lines changed

10 files changed

+670
-6
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
3737

3838
- name: Initialize CodeQL
39-
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
39+
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # pin@v2
4040
with:
4141
languages: 'java'
4242

@@ -45,4 +45,4 @@ jobs:
4545
./gradlew buildForCodeQL --no-build-cache
4646
4747
- name: Perform CodeQL Analysis
48-
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
48+
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # pin@v2

sentry-android-distribution/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ android {
1111

1212
defaultConfig { minSdk = libs.versions.minSdk.get().toInt() }
1313
buildFeatures { buildConfig = false }
14+
15+
testOptions {
16+
unitTests.apply {
17+
isReturnDefaultValues = true
18+
isIncludeAndroidResources = true
19+
}
20+
}
1421
}
1522

1623
kotlin {
@@ -29,4 +36,8 @@ dependencies {
2936
libs.jetbrains.annotations
3037
) // Use implementation instead of compileOnly to override kotlin stdlib's version
3138
implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid))
39+
testImplementation(libs.androidx.test.ext.junit)
40+
testImplementation(libs.roboelectric)
41+
testImplementation(libs.kotlin.test.junit)
42+
testImplementation(libs.androidx.test.core)
3243
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package io.sentry.android.distribution
2+
3+
import io.sentry.SentryLevel
4+
import io.sentry.SentryOptions
5+
import java.io.BufferedReader
6+
import java.io.IOException
7+
import java.io.InputStreamReader
8+
import java.net.HttpURLConnection
9+
import java.net.URL
10+
import java.net.URLEncoder
11+
import javax.net.ssl.HttpsURLConnection
12+
13+
/** HTTP client for making requests to Sentry's distribution API. */
14+
internal class DistributionHttpClient(private val options: SentryOptions) {
15+
16+
/** Represents the result of an HTTP request. */
17+
data class HttpResponse(
18+
val statusCode: Int,
19+
val body: String,
20+
val isSuccessful: Boolean = statusCode in 200..299,
21+
)
22+
23+
/** Parameters for checking updates. */
24+
data class UpdateCheckParams(
25+
val mainBinaryIdentifier: String,
26+
val appId: String,
27+
val platform: String = "android",
28+
val versionCode: Long,
29+
val versionName: String,
30+
)
31+
32+
/**
33+
* Makes a GET request to the distribution API to check for updates.
34+
*
35+
* @param params Update check parameters
36+
* @return HttpResponse containing the response details
37+
*/
38+
fun checkForUpdates(params: UpdateCheckParams): HttpResponse {
39+
val distributionOptions = options.distribution
40+
val orgSlug = distributionOptions.orgSlug
41+
val projectSlug = distributionOptions.projectSlug
42+
val authToken = distributionOptions.orgAuthToken
43+
val baseUrl = distributionOptions.sentryBaseUrl
44+
45+
if (orgSlug.isNullOrEmpty() || projectSlug.isNullOrEmpty() || authToken.isNullOrEmpty()) {
46+
throw IllegalStateException(
47+
"Missing required distribution configuration: orgSlug, projectSlug, or orgAuthToken"
48+
)
49+
}
50+
51+
val urlString = buildString {
52+
append(baseUrl.trimEnd('/'))
53+
append(
54+
"/api/0/projects/${URLEncoder.encode(orgSlug, "UTF-8")}/${URLEncoder.encode(projectSlug, "UTF-8")}/preprodartifacts/check-for-updates/"
55+
)
56+
append("?main_binary_identifier=${URLEncoder.encode(params.mainBinaryIdentifier, "UTF-8")}")
57+
append("&app_id=${URLEncoder.encode(params.appId, "UTF-8")}")
58+
append("&platform=${URLEncoder.encode(params.platform, "UTF-8")}")
59+
append("&build_number=${URLEncoder.encode(params.versionCode.toString(), "UTF-8")}")
60+
append("&build_version=${URLEncoder.encode(params.versionName, "UTF-8")}")
61+
}
62+
val url = URL(urlString)
63+
64+
return try {
65+
makeRequest(url, authToken)
66+
} catch (e: IOException) {
67+
options.logger.log(SentryLevel.ERROR, e, "Network error while checking for updates")
68+
throw e
69+
}
70+
}
71+
72+
private fun makeRequest(url: URL, authToken: String): HttpResponse {
73+
val connection = url.openConnection() as HttpURLConnection
74+
75+
try {
76+
connection.requestMethod = "GET"
77+
connection.setRequestProperty("Authorization", "Bearer $authToken")
78+
connection.setRequestProperty("Accept", "application/json")
79+
connection.setRequestProperty(
80+
"User-Agent",
81+
options.sentryClientName ?: throw IllegalStateException("sentryClientName must be set"),
82+
)
83+
connection.connectTimeout = options.connectionTimeoutMillis
84+
connection.readTimeout = options.readTimeoutMillis
85+
86+
if (connection is HttpsURLConnection && options.sslSocketFactory != null) {
87+
connection.sslSocketFactory = options.sslSocketFactory
88+
}
89+
90+
val responseCode = connection.responseCode
91+
val responseBody = readResponse(connection)
92+
93+
options.logger.log(
94+
SentryLevel.DEBUG,
95+
"Distribution API request completed with status: $responseCode",
96+
)
97+
98+
return HttpResponse(responseCode, responseBody)
99+
} finally {
100+
connection.disconnect()
101+
}
102+
}
103+
104+
private fun readResponse(connection: HttpURLConnection): String {
105+
val inputStream =
106+
if (connection.responseCode in 200..299) {
107+
connection.inputStream
108+
} else {
109+
connection.errorStream ?: connection.inputStream
110+
}
111+
112+
return inputStream?.use { stream ->
113+
BufferedReader(InputStreamReader(stream, "UTF-8")).use { reader -> reader.readText() }
114+
} ?: ""
115+
}
116+
}

sentry-android-distribution/src/main/java/io/sentry/android/distribution/DistributionIntegration.kt

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package io.sentry.android.distribution
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.content.pm.PackageManager
56
import android.net.Uri
7+
import android.os.Build
68
import io.sentry.IDistributionApi
79
import io.sentry.IScopes
810
import io.sentry.Integration
11+
import io.sentry.SentryLevel
912
import io.sentry.SentryOptions
1013
import io.sentry.UpdateInfo
1114
import io.sentry.UpdateStatus
15+
import java.net.SocketTimeoutException
16+
import java.net.UnknownHostException
1217
import org.jetbrains.annotations.ApiStatus
1318

1419
/**
@@ -24,6 +29,9 @@ public class DistributionIntegration(context: Context) : Integration, IDistribut
2429
private lateinit var sentryOptions: SentryOptions
2530
private val context: Context = context.applicationContext
2631

32+
private lateinit var httpClient: DistributionHttpClient
33+
private lateinit var responseParser: UpdateResponseParser
34+
2735
/**
2836
* Registers the Distribution integration with Sentry.
2937
*
@@ -34,6 +42,10 @@ public class DistributionIntegration(context: Context) : Integration, IDistribut
3442
// Store scopes and options for use by distribution functionality
3543
this.scopes = scopes
3644
this.sentryOptions = options
45+
46+
// Initialize HTTP client and response parser
47+
this.httpClient = DistributionHttpClient(options)
48+
this.responseParser = UpdateResponseParser(options)
3749
}
3850

3951
/**
@@ -44,7 +56,31 @@ public class DistributionIntegration(context: Context) : Integration, IDistribut
4456
* @return UpdateStatus indicating if an update is available, up to date, or error
4557
*/
4658
public override fun checkForUpdateBlocking(): UpdateStatus {
47-
throw NotImplementedError()
59+
return try {
60+
sentryOptions.logger.log(SentryLevel.DEBUG, "Checking for distribution updates")
61+
62+
val params = createUpdateCheckParams()
63+
val response = httpClient.checkForUpdates(params)
64+
responseParser.parseResponse(response.statusCode, response.body)
65+
} catch (e: IllegalStateException) {
66+
sentryOptions.logger.log(SentryLevel.WARNING, e.message ?: "Configuration error")
67+
UpdateStatus.UpdateError(e.message ?: "Configuration error")
68+
} catch (e: UnknownHostException) {
69+
// UnknownHostException typically indicates no internet connection available
70+
sentryOptions.logger.log(
71+
SentryLevel.ERROR,
72+
e,
73+
"DNS lookup failed - check internet connection",
74+
)
75+
UpdateStatus.NoNetwork("No internet connection or invalid server URL")
76+
} catch (e: SocketTimeoutException) {
77+
// SocketTimeoutException could indicate either slow network or server issues
78+
sentryOptions.logger.log(SentryLevel.ERROR, e, "Network request timed out")
79+
UpdateStatus.NoNetwork("Request timed out - check network connection")
80+
} catch (e: Exception) {
81+
sentryOptions.logger.log(SentryLevel.ERROR, e, "Unexpected error checking for updates")
82+
UpdateStatus.UpdateError("Unexpected error: ${e.message}")
83+
}
4884
}
4985

5086
/**
@@ -75,4 +111,37 @@ public class DistributionIntegration(context: Context) : Integration, IDistribut
75111
// Silently fail as this is expected behavior in some environments
76112
}
77113
}
114+
115+
private fun createUpdateCheckParams(): DistributionHttpClient.UpdateCheckParams {
116+
return try {
117+
val packageManager = context.packageManager
118+
val packageName = context.packageName
119+
val packageInfo =
120+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
121+
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
122+
} else {
123+
@Suppress("DEPRECATION") packageManager.getPackageInfo(packageName, 0)
124+
}
125+
126+
val versionName = packageInfo.versionName ?: "unknown"
127+
val versionCode =
128+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
129+
packageInfo.longVersionCode
130+
} else {
131+
@Suppress("DEPRECATION") packageInfo.versionCode.toLong()
132+
}
133+
val appId = context.applicationInfo.packageName
134+
135+
DistributionHttpClient.UpdateCheckParams(
136+
mainBinaryIdentifier = appId,
137+
appId = appId,
138+
platform = "android",
139+
versionCode = versionCode,
140+
versionName = versionName,
141+
)
142+
} catch (e: PackageManager.NameNotFoundException) {
143+
sentryOptions.logger.log(SentryLevel.ERROR, e, "Failed to get package info")
144+
throw IllegalStateException("Unable to get app package information", e)
145+
}
146+
}
78147
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.sentry.android.distribution
2+
3+
import io.sentry.SentryLevel
4+
import io.sentry.SentryOptions
5+
import io.sentry.UpdateInfo
6+
import io.sentry.UpdateStatus
7+
import org.json.JSONException
8+
import org.json.JSONObject
9+
10+
/** Parser for distribution API responses. */
11+
internal class UpdateResponseParser(private val options: SentryOptions) {
12+
13+
/**
14+
* Parses the API response and returns the appropriate UpdateStatus.
15+
*
16+
* @param statusCode HTTP status code
17+
* @param responseBody Response body as string
18+
* @return UpdateStatus indicating the result
19+
*/
20+
fun parseResponse(statusCode: Int, responseBody: String): UpdateStatus {
21+
return when (statusCode) {
22+
200 -> parseSuccessResponse(responseBody)
23+
in 400..499 -> UpdateStatus.UpdateError("Client error: $statusCode")
24+
in 500..599 -> UpdateStatus.UpdateError("Server error: $statusCode")
25+
else -> UpdateStatus.UpdateError("Unexpected response code: $statusCode")
26+
}
27+
}
28+
29+
private fun parseSuccessResponse(responseBody: String): UpdateStatus {
30+
return try {
31+
val json = JSONObject(responseBody)
32+
33+
options.logger.log(SentryLevel.DEBUG, "Parsing distribution API response")
34+
35+
// Check if there's a new release available
36+
val updateAvailable = json.optBoolean("updateAvailable", false)
37+
38+
if (updateAvailable) {
39+
val updateInfo = parseUpdateInfo(json)
40+
UpdateStatus.NewRelease(updateInfo)
41+
} else {
42+
UpdateStatus.UpToDate.getInstance()
43+
}
44+
} catch (e: JSONException) {
45+
options.logger.log(SentryLevel.ERROR, e, "Failed to parse API response")
46+
UpdateStatus.UpdateError("Invalid response format: ${e.message}")
47+
} catch (e: Exception) {
48+
options.logger.log(SentryLevel.ERROR, e, "Unexpected error parsing response")
49+
UpdateStatus.UpdateError("Failed to parse response: ${e.message}")
50+
}
51+
}
52+
53+
private fun parseUpdateInfo(json: JSONObject): UpdateInfo {
54+
val id = json.optString("id", "")
55+
val buildVersion = json.optString("buildVersion", "")
56+
val buildNumber = json.optInt("buildNumber", 0)
57+
val downloadUrl = json.optString("downloadUrl", "")
58+
val appName = json.optString("appName", "")
59+
val createdDate = json.optString("createdDate", "")
60+
61+
// Validate required fields (optString returns "null" for null values)
62+
val missingFields = mutableListOf<String>()
63+
64+
if (id.isEmpty() || id == "null") {
65+
missingFields.add("id")
66+
}
67+
if (buildVersion.isEmpty() || buildVersion == "null") {
68+
missingFields.add("buildVersion")
69+
}
70+
if (downloadUrl.isEmpty() || downloadUrl == "null") {
71+
missingFields.add("downloadUrl")
72+
}
73+
74+
if (missingFields.isNotEmpty()) {
75+
throw IllegalArgumentException(
76+
"Missing required fields in API response: ${missingFields.joinToString(", ")}"
77+
)
78+
}
79+
80+
return UpdateInfo(id, buildVersion, buildNumber, downloadUrl, appName, createdDate)
81+
}
82+
}

0 commit comments

Comments
 (0)