Skip to content

Instantly share code, notes, and snippets.

@angusholder
Last active July 10, 2019 13:44
Show Gist options
  • Save angusholder/426b26f2890adfd349d4884f9ff9f1cf to your computer and use it in GitHub Desktop.
Save angusholder/426b26f2890adfd349d4884f9ff9f1cf to your computer and use it in GitHub Desktop.
Fixes the slow startup caused by Sentry accessing Java resources on Android. Instead we access files using Android assets. Put your Sentry config in /app/src/main/assets/sentry.properties
package com.example
import android.content.Context
import android.util.Log
import io.sentry.SentryUncaughtExceptionHandler
import io.sentry.android.AndroidSentryClientFactory
import io.sentry.buffer.Buffer
import io.sentry.buffer.DiskBuffer
import io.sentry.connection.AsyncConnection
import io.sentry.connection.BufferedConnection
import io.sentry.dsn.Dsn
import io.sentry.event.interfaces.StackTraceInterface
import io.sentry.marshaller.json.JsonMarshaller
import io.sentry.util.Util
import java.io.File
import java.io.InputStream
import java.util.*
import java.util.concurrent.RejectedExecutionHandler
import java.util.concurrent.ThreadPoolExecutor
class FixedAndroidSentryClientFactory(private val context: Context) : AndroidSentryClientFactory(context) {
companion object {
private val TAG = "FixedAndroidSentryClFac"
private fun getConfigFilePath(): String {
var filePath = System.getProperty("sentry.properties.file")
if (filePath == null) {
filePath = System.getenv("SENTRY_PROPERTIES_FILE")
}
if (filePath == null) {
filePath = "sentry.properties"
}
return filePath
}
private var configProps: Properties? = null
private fun getProps(context: Context): Properties {
var props = configProps
if (props != null) return props
val filePath = getConfigFilePath()
var input: InputStream? = null
try {
input = context.assets.open(filePath)
if (input != null) {
props = Properties()
props.load(input)
} else {
Log.d(TAG, "Sentry configuration file not found in filesystem or classpath: '$filePath'.")
}
} catch (e: Exception) {
Log.e(TAG, "Error loading Sentry configuration file '$filePath': $e")
} finally {
input?.close()
}
if (props == null) {
props = Properties()
}
configProps = props
return props
}
}
private fun lookup(key: String, dsn: Dsn? = null): String? {
var value: String? = null
// Try to obtain from the provided DSN, if set
if (value == null && dsn != null) {
value = dsn.options[key]
if (value != null) {
Log.d(TAG, "Found $key=$value in DSN.")
}
}
// Try to obtain from config file
if (value == null) {
value = getProps(context).getProperty(key)
if (value != null) {
Log.d(TAG, "Found $key=$value in props.")
}
}
return value?.trim { it <= ' ' }
}
/**
* Returns the list of package names to consider "in-app".
*
*
* Those packages will be used with the [StackTraceInterface] to show frames that are a part of
* the main application in the Sentry UI by default.
*
* @param dsn Sentry server DSN which may contain options.
* @return the list of package names to consider "in-app".
*/
override fun getInAppFrames(dsn: Dsn): Collection<String> {
val inAppFramesOption = lookup(IN_APP_FRAMES_OPTION, dsn)
if (inAppFramesOption.isNullOrEmpty()) {
// Only warn if the user didn't set it at all
if (inAppFramesOption == null) {
Log.w(TAG, "No '$IN_APP_FRAMES_OPTION' was configured, this option is highly recommended as it affects stacktrace grouping and display on Sentry. See documentation: https://docs.sentry.io/clients/java/config/#in-application-stack-frames")
}
return emptyList()
}
return inAppFramesOption
.split(",")
.mapNotNull { it.trim().takeIf(CharSequence::isNotEmpty) }
}
/**
* Whether or not to wrap the underlying connection in an [AsyncConnection].
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether or not to wrap the underlying connection in an [AsyncConnection].
*/
override fun getAsyncEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(ASYNC_OPTION, dsn), ignoreCase = true)
}
/**
* Handler for tasks that cannot be immediately queued by a [ThreadPoolExecutor].
*
* @param dsn Sentry server DSN which may contain options.
* @return Handler for tasks that cannot be immediately queued by a [ThreadPoolExecutor].
*/
override fun getRejectedExecutionHandler(dsn: Dsn): RejectedExecutionHandler {
return ThreadPoolExecutor.DiscardOldestPolicy()
}
/**
* Maximum time to wait for [BufferedConnection] shutdown when closed, in milliseconds.
*
* @param dsn Sentry server DSN which may contain options.
* @return Maximum time to wait for [BufferedConnection] shutdown when closed, in milliseconds.
*/
override fun getBufferedConnectionShutdownTimeout(dsn: Dsn): Long {
return Util.parseLong(lookup(BUFFER_SHUTDOWN_TIMEOUT_OPTION, dsn), BUFFER_SHUTDOWN_TIMEOUT_DEFAULT)!!
}
/**
* Whether or not to attempt a graceful shutdown of the [BufferedConnection] upon close.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether or not to attempt a graceful shutdown of the [BufferedConnection] upon close.
*/
override fun getBufferedConnectionGracefulShutdownEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(BUFFER_GRACEFUL_SHUTDOWN_OPTION, dsn), ignoreCase = true)
}
/**
* How long to wait between attempts to flush the disk buffer, in milliseconds.
*
* @param dsn Sentry server DSN which may contain options.
* @return ow long to wait between attempts to flush the disk buffer, in milliseconds.
*/
override fun getBufferFlushtime(dsn: Dsn): Long {
return Util.parseLong(lookup(BUFFER_FLUSHTIME_OPTION, dsn), BUFFER_FLUSHTIME_DEFAULT)!!
}
/**
* The graceful shutdown timeout of the async executor, in milliseconds.
*
* @param dsn Sentry server DSN which may contain options.
* @return The graceful shutdown timeout of the async executor, in milliseconds.
*/
override fun getAsyncShutdownTimeout(dsn: Dsn): Long {
return Util.parseLong(lookup(ASYNC_SHUTDOWN_TIMEOUT_OPTION, dsn), ASYNC_SHUTDOWN_TIMEOUT_DEFAULT)!!
}
/**
* Whether or not to attempt the graceful shutdown of the [AsyncConnection] upon close.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether or not to attempt the graceful shutdown of the [AsyncConnection] upon close.
*/
override fun getAsyncGracefulShutdownEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(ASYNC_GRACEFUL_SHUTDOWN_OPTION, dsn), ignoreCase = true)
}
/**
* Maximum size of the async send queue.
*
* @param dsn Sentry server DSN which may contain options.
* @return Maximum size of the async send queue.
*/
override fun getAsyncQueueSize(dsn: Dsn): Int {
return Util.parseInteger(lookup(ASYNC_QUEUE_SIZE_OPTION, dsn), QUEUE_SIZE_DEFAULT)!!
}
/**
* Priority of threads used for the async connection.
*
* @param dsn Sentry server DSN which may contain options.
* @return Priority of threads used for the async connection.
*/
override fun getAsyncPriority(dsn: Dsn): Int {
return Util.parseInteger(lookup(ASYNC_PRIORITY_OPTION, dsn), Thread.MIN_PRIORITY)!!
}
/**
* The number of threads used for the async connection.
*
* @param dsn Sentry server DSN which may contain options.
* @return The number of threads used for the async connection.
*/
override fun getAsyncThreads(dsn: Dsn): Int {
return Util.parseInteger(lookup(ASYNC_THREADS_OPTION, dsn),
Runtime.getRuntime().availableProcessors())!!
}
/**
* Whether to disable security checks over an SSL connection.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether to disable security checks over an SSL connection.
*/
override fun getBypassSecurityEnabled(dsn: Dsn): Boolean {
return dsn.protocolSettings.contains(NAIVE_PROTOCOL)
}
/**
* Whether to sample events, and if so how much to allow through to the server (from 0.0 to 1.0).
*
* @param dsn Sentry server DSN which may contain options.
* @return The ratio of events to allow through to server, or null if sampling is disabled.
*/
override fun getSampleRate(dsn: Dsn): Double? {
return Util.parseDouble(lookup(SAMPLE_RATE_OPTION, dsn), null)
}
/**
* HTTP proxy port for Sentry connections.
*
* @param dsn Sentry server DSN which may contain options.
* @return HTTP proxy port for Sentry connections.
*/
override fun getProxyPort(dsn: Dsn): Int {
return Util.parseInteger(lookup(HTTP_PROXY_PORT_OPTION, dsn), HTTP_PROXY_PORT_DEFAULT)!!
}
/**
* HTTP proxy hostname for Sentry connections.
*
* @param dsn Sentry server DSN which may contain options.
* @return HTTP proxy hostname for Sentry connections.
*/
override fun getProxyHost(dsn: Dsn): String? {
return lookup(HTTP_PROXY_HOST_OPTION, dsn)
}
/**
* HTTP proxy username for Sentry connections.
*
* @param dsn Sentry server DSN which may contain options.
* @return HTTP proxy username for Sentry connections.
*/
override fun getProxyUser(dsn: Dsn): String? {
return lookup(HTTP_PROXY_USER_OPTION, dsn)
}
/**
* HTTP proxy password for Sentry connections.
*
* @param dsn Sentry server DSN which may contain options.
* @return HTTP proxy password for Sentry connections.
*/
override fun getProxyPass(dsn: Dsn): String? {
return lookup(HTTP_PROXY_PASS_OPTION, dsn)
}
/**
* Application version to send with [io.sentry.event.Event]s that don't already
* have a value for the field set.
*
* @param dsn Sentry server DSN which may contain options.
* @return Application version to send with [io.sentry.event.Event]s.
*/
override fun getRelease(dsn: Dsn): String? {
return lookup(RELEASE_OPTION, dsn)
}
/**
* Application distribution to send with [io.sentry.event.Event]s that don't already
* have a value for the field set.
*
* @param dsn Sentry server DSN which may contain options.
* @return Application version to send with [io.sentry.event.Event]s.
*/
override fun getDist(dsn: Dsn): String? {
return lookup(DIST_OPTION, dsn)
}
/**
* Application environmentribution to send with [io.sentry.event.Event]s that don't already
* have a value for the field set.
*
* @param dsn Sentry server DSN which may contain options.
* @return Application version to send with [io.sentry.event.Event]s.
*/
override fun getEnvironment(dsn: Dsn): String? {
return lookup(ENVIRONMENT_OPTION, dsn)
}
/**
* Server name to send with [io.sentry.event.Event]s that don't already
* have a value for the field set.
*
* @param dsn Sentry server DSN which may contain options.
* @return Server name to send with [io.sentry.event.Event]s.
*/
override fun getServerName(dsn: Dsn): String? {
return lookup(SERVERNAME_OPTION, dsn)
}
/**
* Additional tags to send with [io.sentry.event.Event]s.
*
* @param dsn Sentry server DSN which may contain options.
* @return Additional tags to send with [io.sentry.event.Event]s.
*/
override fun getTags(dsn: Dsn): Map<String, String> {
return Util.parseTags(lookup(TAGS_OPTION, dsn))
}
/**
* Tags to extract from the MDC system and set on [io.sentry.event.Event]s, where applicable.
*
* @param dsn Sentry server DSN which may contain options.
* @return Tags to extract from the MDC system and set on [io.sentry.event.Event]s, where applicable.
*/
@Suppress("DEPRECATION")
override fun getMdcTags(dsn: Dsn): Set<String> {
var value = lookup(MDCTAGS_OPTION, dsn)
if (Util.isNullOrEmpty(value)) {
value = lookup(EXTRATAGS_OPTION, dsn)
if (!Util.isNullOrEmpty(value)) {
Log.w(TAG, "The '$EXTRATAGS_OPTION' option is deprecated, please use the '$MDCTAGS_OPTION' option instead.")
}
}
return Util.parseMdcTags(value)
}
/**
* Extra data to send with [io.sentry.event.Event]s.
*
* @param dsn Sentry server DSN which may contain options.
* @return Extra data to send with [io.sentry.event.Event]s.
*/
override fun getExtra(dsn: Dsn): Map<String, String> {
return Util.parseExtra(lookup(EXTRA_OPTION, dsn))
}
/**
* Whether to compress requests sent to the Sentry Server.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether to compress requests sent to the Sentry Server.
*/
override fun getCompressionEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(COMPRESSION_OPTION, dsn), ignoreCase = true)
}
/**
* Whether to hide common stackframes with enclosing exceptions.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether to hide common stackframes with enclosing exceptions.
*/
override fun getHideCommonFramesEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(HIDE_COMMON_FRAMES_OPTION, dsn), ignoreCase = true)
}
/**
* The maximum length of the message body in the requests to the Sentry Server.
*
* @param dsn Sentry server DSN which may contain options.
* @return The maximum length of the message body in the requests to the Sentry Server.
*/
override fun getMaxMessageLength(dsn: Dsn): Int {
return Util.parseInteger(
lookup(MAX_MESSAGE_LENGTH_OPTION, dsn), JsonMarshaller.DEFAULT_MAX_MESSAGE_LENGTH)!!
}
/**
* Timeout for requests to the Sentry server, in milliseconds.
*
* @param dsn Sentry server DSN which may contain options.
* @return Timeout for requests to the Sentry server, in milliseconds.
*/
override fun getTimeout(dsn: Dsn): Int {
return Util.parseInteger(lookup(TIMEOUT_OPTION, dsn), TIMEOUT_DEFAULT)!!
}
/**
* Whether or not buffering is enabled.
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether or not buffering is enabled.
*/
override fun getBufferEnabled(dsn: Dsn): Boolean {
val bufferEnabled = lookup(BUFFER_ENABLED_OPTION, dsn)
return if (bufferEnabled != null) {
java.lang.Boolean.parseBoolean(bufferEnabled)
} else BUFFER_ENABLED_DEFAULT
}
/**
* Get the [Buffer] where events are stored when network is down.
*
* @param dsn Dsn passed in by the user.
* @return the [Buffer] where events are stored when network is down.
*/
override fun getBuffer(dsn: Dsn): Buffer? {
val bufferDir = lookup(BUFFER_DIR_OPTION, dsn)
return if (bufferDir != null) {
DiskBuffer(File(bufferDir), getBufferSize(dsn))
} else null
}
/**
* Get the maximum number of events to cache offline when network is down.
*
* @param dsn Dsn passed in by the user.
* @return the maximum number of events to cache offline when network is down.
*/
override fun getBufferSize(dsn: Dsn): Int {
return Util.parseInteger(lookup(BUFFER_SIZE_OPTION, dsn), BUFFER_SIZE_DEFAULT)!!
}
/**
* Whether or not to enable a [SentryUncaughtExceptionHandler].
*
* @param dsn Sentry server DSN which may contain options.
* @return Whether or not to enable a [SentryUncaughtExceptionHandler].
*/
override fun getUncaughtHandlerEnabled(dsn: Dsn): Boolean {
return !"false".equals(lookup(UNCAUGHT_HANDLER_ENABLED_OPTION, dsn), ignoreCase = true)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment