Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies {
compileOnly(projects.sentryAndroidTimber)
compileOnly(projects.sentryAndroidReplay)
compileOnly(projects.sentryCompose)
compileOnly(projects.sentryAndroidDistribution)

// lifecycle processor, session tracking
implementation(libs.androidx.lifecycle.common.java8)
Expand Down
5 changes: 5 additions & 0 deletions sentry-android-core/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@
-dontwarn io.sentry.android.replay.DefaultReplayBreadcrumbConverter
-keepnames class io.sentry.android.replay.ReplayIntegration
##---------------End: proguard configuration for sentry-android-replay ----------

##---------------Begin: proguard configuration for sentry-android-distribution ----------
-dontwarn io.sentry.android.distribution.internal.DistributionIntegration
-keepnames class io.sentry.android.distribution.internal.DistributionIntegration
##---------------End: proguard configuration for sentry-android-distribution ----------
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.distribution.internal.DistributionIntegration;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.replay.DefaultReplayBreadcrumbConverter;
import io.sentry.android.replay.ReplayIntegration;
Expand Down Expand Up @@ -321,7 +322,8 @@ static void installDefaultIntegrations(
final @NotNull ActivityFramesTracker activityFramesTracker,
final boolean isFragmentAvailable,
final boolean isTimberAvailable,
final boolean isReplayAvailable) {
final boolean isReplayAvailable,
final boolean isDistributionAvailable) {

// Integration MUST NOT cache option values in ctor, as they will be configured later by the
// user
Expand Down Expand Up @@ -391,6 +393,9 @@ static void installDefaultIntegrations(
options.addIntegration(replay);
options.setReplayController(replay);
}
if (isDistributionAvailable) {
options.addIntegration(new DistributionIntegration());
}
options
.getFeedbackOptions()
.setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public final class SentryAndroid {
static final String SENTRY_REPLAY_INTEGRATION_CLASS_NAME =
"io.sentry.android.replay.ReplayIntegration";

static final String SENTRY_DISTRIBUTION_INTEGRATION_CLASS_NAME =
"io.sentry.android.distribution.internal.DistributionIntegration";

private static final String TIMBER_CLASS_NAME = "timber.log.Timber";
private static final String FRAGMENT_CLASS_NAME =
"androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks";
Expand Down Expand Up @@ -111,6 +114,8 @@ public static void init(
&& classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options));
final boolean isReplayAvailable =
classLoader.isClassAvailable(SENTRY_REPLAY_INTEGRATION_CLASS_NAME, options);
final boolean isDistributionAvailable =
classLoader.isClassAvailable(SENTRY_DISTRIBUTION_INTEGRATION_CLASS_NAME, options);

final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger);
final io.sentry.util.LoadClass loadClass = new io.sentry.util.LoadClass();
Expand All @@ -131,7 +136,8 @@ public static void init(
activityFramesTracker,
isFragmentAvailable,
isTimberAvailable,
isReplayAvailable);
isReplayAvailable,
isDistributionAvailable);

try {
configuration.configure(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class AndroidContinuousProfilerTest {
false,
false,
false,
false,
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class AndroidOptionsInitializerTest {
false,
false,
false,
false,
)

sentryOptions.configureOptions()
Expand Down Expand Up @@ -149,6 +150,7 @@ class AndroidOptionsInitializerTest {
isFragmentAvailable,
isTimberAvailable,
isReplayAvailable,
false,
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down Expand Up @@ -820,6 +822,7 @@ class AndroidOptionsInitializerTest {
false,
false,
false,
false,
)
verify(mockOptions, never()).outboxPath
verify(mockOptions, never()).cacheDirPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class AndroidProfilerTest {
false,
false,
false,
false,
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class AndroidTransactionProfilerTest {
false,
false,
false,
false,
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class SentryInitProviderTest {
false,
false,
false,
false,
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down
75 changes: 75 additions & 0 deletions sentry-android-distribution/api/sentry-android-distribution.api
Original file line number Diff line number Diff line change
@@ -1,4 +1,79 @@
public final class io/sentry/android/distribution/Distribution {
public static final field INSTANCE Lio/sentry/android/distribution/Distribution;
public final fun checkForUpdate (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)V
public final fun checkForUpdateBlocking (Landroid/content/Context;)Lio/sentry/android/distribution/UpdateStatus;
public final fun downloadUpdate (Landroid/content/Context;Lio/sentry/android/distribution/UpdateInfo;)V
public final fun init (Landroid/content/Context;)V
public final fun init (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)V
public final fun isEnabled ()Z
}

public final class io/sentry/android/distribution/DistributionOptions {
public fun <init> ()V
public final fun getBuildConfiguration ()Ljava/lang/String;
public final fun getOrgAuthToken ()Ljava/lang/String;
public final fun getOrganizationSlug ()Ljava/lang/String;
public final fun getProjectSlug ()Ljava/lang/String;
public final fun getSentryBaseUrl ()Ljava/lang/String;
public final fun setBuildConfiguration (Ljava/lang/String;)V
public final fun setOrgAuthToken (Ljava/lang/String;)V
public final fun setOrganizationSlug (Ljava/lang/String;)V
public final fun setProjectSlug (Ljava/lang/String;)V
public final fun setSentryBaseUrl (Ljava/lang/String;)V
}

public final class io/sentry/android/distribution/UpdateInfo {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()I
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/sentry/android/distribution/UpdateInfo;
public static synthetic fun copy$default (Lio/sentry/android/distribution/UpdateInfo;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/sentry/android/distribution/UpdateInfo;
public fun equals (Ljava/lang/Object;)Z
public final fun getAppName ()Ljava/lang/String;
public final fun getBuildNumber ()I
public final fun getBuildVersion ()Ljava/lang/String;
public final fun getCreatedDate ()Ljava/lang/String;
public final fun getDownloadUrl ()Ljava/lang/String;
public final fun getId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public abstract class io/sentry/android/distribution/UpdateStatus {
}

public final class io/sentry/android/distribution/UpdateStatus$Error : io/sentry/android/distribution/UpdateStatus {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lio/sentry/android/distribution/UpdateStatus$Error;
public static synthetic fun copy$default (Lio/sentry/android/distribution/UpdateStatus$Error;Ljava/lang/String;ILjava/lang/Object;)Lio/sentry/android/distribution/UpdateStatus$Error;
public fun equals (Ljava/lang/Object;)Z
public final fun getMessage ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/sentry/android/distribution/UpdateStatus$NewRelease : io/sentry/android/distribution/UpdateStatus {
public fun <init> (Lio/sentry/android/distribution/UpdateInfo;)V
public final fun component1 ()Lio/sentry/android/distribution/UpdateInfo;
public final fun copy (Lio/sentry/android/distribution/UpdateInfo;)Lio/sentry/android/distribution/UpdateStatus$NewRelease;
public static synthetic fun copy$default (Lio/sentry/android/distribution/UpdateStatus$NewRelease;Lio/sentry/android/distribution/UpdateInfo;ILjava/lang/Object;)Lio/sentry/android/distribution/UpdateStatus$NewRelease;
public fun equals (Ljava/lang/Object;)Z
public final fun getInfo ()Lio/sentry/android/distribution/UpdateInfo;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/sentry/android/distribution/UpdateStatus$UpToDate : io/sentry/android/distribution/UpdateStatus {
public static final field INSTANCE Lio/sentry/android/distribution/UpdateStatus$UpToDate;
}

public final class io/sentry/android/distribution/internal/DistributionIntegration : io/sentry/Integration {
public fun <init> ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

3 changes: 3 additions & 0 deletions sentry-android-distribution/src/main/AndroidManifest.xml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not needed anymore if we define a namespace in the gradle config

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -1,3 +1,87 @@
package io.sentry.android.distribution

public class Distribution {}
import android.content.Context
import android.content.Intent
import android.net.Uri
import io.sentry.android.distribution.internal.DistributionInternal

/**
* The public Android SDK for Sentry Build Distribution.
*
* Provides functionality to check for app updates and download new versions from Sentry's preprod
* artifacts system.
*/
public object Distribution {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost exactly the same API as in the Emerge Build Distribution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new-ish features like logging and replay, we're exposing top level entry points under the Sentry class. E.g. Sentry.logger() and Sentry.replay(). It could make sense to do the same for distribution as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point, I looked in to it and it seems like a bigger change, I will add this in a future PR.

/**
* Initialize build distribution with default options. This should be called once per process,
* typically in Application.onCreate().
*
* @param context Android context
*/
public fun init(context: Context) {
init(context) {}
}

/**
* Initialize build distribution with the provided configuration. This should be called once per
* process, typically in Application.onCreate().
*
* @param context Android context
* @param configuration Configuration handler for build distribution options
*/
public fun init(context: Context, configuration: (DistributionOptions) -> Unit) {
val options = DistributionOptions()
configuration(options)
DistributionInternal.init(context, options)
}

/**
* Check if build distribution is enabled and properly configured.
*
* @return true if build distribution is enabled
*/
public fun isEnabled(): Boolean {
return DistributionInternal.isEnabled()
}

/**
* Check for available updates synchronously (blocking call). This method will block the calling
* thread while making the network request. Consider using checkForUpdate with callback for
* non-blocking behavior.
*
* @param context Android context
* @return UpdateStatus indicating if an update is available, up to date, or error
*/
public fun checkForUpdateBlocking(context: Context): UpdateStatus {
return DistributionInternal.checkForUpdateBlocking(context)
}

/**
* Check for available updates asynchronously using a callback.
*
* @param context Android context
* @param onResult Callback that will be called with the UpdateStatus result
*/
public fun checkForUpdate(context: Context, onResult: (UpdateStatus) -> Unit) {
DistributionInternal.checkForUpdateAsync(context, onResult)
}

/**
* Download and install the provided update by opening the download URL in the default browser or
* appropriate application.
*
* @param context Android context
* @param info Information about the update to download
*/
public fun downloadUpdate(context: Context, info: UpdateInfo) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(info.downloadUrl))
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

try {
context.startActivity(browserIntent)
} catch (e: android.content.ActivityNotFoundException) {
// No application can handle the HTTP/HTTPS URL, typically no browser installed
// Silently fail as this is expected behavior in some environments
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.sentry.android.distribution

/** Configuration options for Sentry Build Distribution. */
public class DistributionOptions {
/** Organization authentication token for API access */
public var orgAuthToken: String = ""

/** Sentry organization slug */
public var organizationSlug: String = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a nit, but would be nice to be consistent with naming - i.e. either call this orgSlug or change the above to organizationAuthToken

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I’ll do this in a follow up


/** Sentry project slug */
public var projectSlug: String = ""

/** Base URL for Sentry API (defaults to https://sentry.io) */
public var sentryBaseUrl: String = "https://sentry.io"

/** Optional build configuration name for filtering (e.g., "debug", "release", "staging") */
public var buildConfiguration: String? = null
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it is part of the public API, should we also use regular class here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will do in a follow up

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.sentry.android.distribution

/**
* Information about an available app update.
*
* @param id Unique identifier for this build artifact
* @param buildVersion Version string (e.g., "1.2.0")
* @param buildNumber Build number for this version
* @param downloadUrl URL where the update can be downloaded
* @param appName Application name
* @param createdDate ISO timestamp when this build was created
*/
public data class UpdateInfo(
val id: String,
val buildVersion: String,
val buildNumber: Int,
val downloadUrl: String,
val appName: String,
val createdDate: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.sentry.android.distribution

/** Represents the result of checking for app updates. */
public sealed class UpdateStatus {
/** Current app version is up to date, no update available. */
public object UpToDate : UpdateStatus()

/** A new release is available for download. */
public data class NewRelease(val info: UpdateInfo) : UpdateStatus()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here about using regular classes


/** An error occurred during the update check. */
public data class Error(val message: String) : UpdateStatus()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry.android.distribution.internal
Copy link
Member

@romtsn romtsn Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking if we should move this out of the internal package? Because this will be the entry point for this submodule/integration and be accessed publicly from within sentry-android-core

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, hmm i messed this up then, I think I should have made the Distribution class the entry point. thanks for catching this!


import io.sentry.IScopes
import io.sentry.Integration
import io.sentry.SentryOptions

/**
* Integration that automatically enables distribution functionality when the module is included.
*/
public class DistributionIntegration : Integration {
public override fun register(scopes: IScopes, options: SentryOptions) {
// Distribution integration automatically enables when module is present
// No configuration needed - just having this class on the classpath enables the feature

// If needed, we could initialize DistributionInternal here in the future
// For now, Distribution.init() still needs to be called manually
}
}
Loading
Loading