Skip to content
Draft
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 platform/jvm/capture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ android {
checkReleaseBuilds = true
disable.add("GradleDependency")
disable.add("AndroidGradlePluginVersion")
disable.add("EnsureInitializerMetadata")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this required? I'm hoping we don't have to ask customers to do this

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture

import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import androidx.startup.Initializer
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl

/**
* Auto-initializes Capture SDK by reading configuration from AndroidManifest <meta-data> tags.
*
* Required meta-data:
* io.bitdrift.capture.API_KEY - The API key for Capture.
*
* Optional meta-data:
* io.bitdrift.capture.API_URL - Custom API URL (default: https://api.bitdrift.io).
* io.bitdrift.capture.SESSION_STRATEGY - "fixed" or "activity_based" (default: activity_based).
* io.bitdrift.capture.INACTIVITY_THRESHOLD_IN_MINUTES - Minutes for activity_based strategy (default: 30).
* io.bitdrift.capture.ENABLE_SESSION_REPLAY - "true"/"false" (default: true).
* io.bitdrift.capture.ENABLE_FATAL_ISSUE_REPORTING - "true"/"false" (default: true).
* io.bitdrift.capture.SLEEP_MODE - "enabled"/"disabled" (default: disabled).
*/
class CaptureInitializer : Initializer<Unit> {
override fun create(context: Context) {
val appContext = context.applicationContext
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we do this then we don't need the ContextHolder dep, or you could do val appContext = ContextHolder.APP_CONTEXT here instead

val metadata =
appContext.packageManager
.getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
.metaData
if (metadata == null) {
Log.w(LOG_TAG, "No metadata found in manifest, skipping auto-init")
return
}

val apiKey = metadata.getString(KEY_API_KEY)
if (apiKey.isNullOrBlank()) {
Log.w(LOG_TAG, "Missing $KEY_API_KEY in manifest, skipping auto-init")
return
}

val apiUrl = metadata.getString(KEY_API_URL)?.let { parseUrl(it) }
val sessionStrategy = buildSessionStrategy(metadata)
val configuration = buildConfiguration(metadata)

Capture.Logger.start(
apiKey = apiKey,
sessionStrategy = sessionStrategy,
configuration = configuration,
apiUrl = apiUrl ?: defaultUrl(),
context = appContext,
)
}

override fun dependencies(): List<Class<out Initializer<*>>> = listOf(ContextHolder::class.java)

private fun buildSessionStrategy(metadata: android.os.Bundle): io.bitdrift.capture.providers.session.SessionStrategy {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we go down this route I'd like to see us wrap all the metadata strongly-typed parsing logic inside our Configuration class so the properties stay in sync

val strategy = metadata.getString(KEY_SESSION_STRATEGY) ?: "activity_based"
return when (strategy.lowercase()) {
"fixed" ->
io.bitdrift.capture.providers.session.SessionStrategy
.Fixed()
else -> {
val threshold = metadata.getInt(KEY_INACTIVITY_THRESHOLD_IN_MINUTES, 30).toLong()
io.bitdrift.capture.providers.session.SessionStrategy.ActivityBased(
inactivityThresholdMins = threshold,
)
}
}
}

private fun buildConfiguration(metadata: android.os.Bundle): Configuration {
val enableReplay = metadata.getBoolean(KEY_ENABLE_SESSION_REPLAY, true)
val enableFatalIssue = metadata.getBoolean(KEY_ENABLE_FATAL_ISSUE_REPORTING, true)
val sleepMode =
when ((metadata.getString(KEY_SLEEP_MODE) ?: "disabled").lowercase()) {
"enabled" -> SleepMode.ENABLED
else -> SleepMode.DISABLED
}
return Configuration(
sessionReplayConfiguration =
if (enableReplay) {
io.bitdrift.capture.replay
.SessionReplayConfiguration()
} else {
null
},
enableFatalIssueReporting = enableFatalIssue,
sleepMode = sleepMode,
)
}

private fun parseUrl(url: String): HttpUrl? =
try {
url.toHttpUrl()
} catch (e: IllegalArgumentException) {
Log.w(LOG_TAG, "Invalid API URL: $url", e)
null
}

private fun defaultUrl() =
HttpUrl
.Builder()
.scheme("https")
.host("api.bitdrift.io")
.build()

/**
* TODO: Just prototyping for now...
*/
companion object {
private const val LOG_TAG = "CaptureInit"
private const val KEY_API_KEY = "io.bitdrift.capture.API_KEY"
private const val KEY_API_URL = "io.bitdrift.capture.API_URL"
private const val KEY_SESSION_STRATEGY = "io.bitdrift.capture.SESSION_STRATEGY"
private const val KEY_INACTIVITY_THRESHOLD_IN_MINUTES = "io.bitdrift.capture.INACTIVITY_THRESHOLD_IN_MINUTES"
private const val KEY_ENABLE_SESSION_REPLAY = "io.bitdrift.capture.ENABLE_SESSION_REPLAY"
private const val KEY_ENABLE_FATAL_ISSUE_REPORTING =
"io.bitdrift.capture.ENABLE_FATAL_ISSUE_REPORTING"
private const val KEY_SLEEP_MODE = "io.bitdrift.capture.SLEEP_MODE"
}
}
26 changes: 20 additions & 6 deletions platform/jvm/gradle-test-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,33 @@

<meta-data android:name="io.sentry.auto-init" android:value="false" />

<!-- Opt in to Bitdrift Capture auto-init -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge"
>
<!-- Disable automatic initialization of Bitdrift ContextHolder -->
tools:node="merge">
<meta-data
android:name="io.bitdrift.capture.ContextHolder"
tools:node="remove"
/>
android:name="io.bitdrift.capture.CaptureInitializer"
android:value="androidx.startup" />
</provider>

<!-- Bitdrift Capture auto-init configuration -->
<meta-data android:name="io.bitdrift.capture.API_KEY"
android:value="YOUR_API_KEY_HERE" />
<meta-data android:name="io.bitdrift.capture.API_URL"
android:value="https://api.bitdrift.io" />
<meta-data android:name="io.bitdrift.capture.SESSION_STRATEGY"
android:value="fixed" />
<meta-data android:name="io.bitdrift.capture.INACTIVITY_THRESHOLD_IN_MINUTES"
android:value="60" />
<meta-data android:name="io.bitdrift.capture.ENABLE_SESSION_REPLAY"
android:value="true" />
<meta-data android:name="io.bitdrift.capture.ENABLE_FATAL_ISSUE_REPORTING"
android:value="true" />
<meta-data android:name="io.bitdrift.capture.SLEEP_MODE"
android:value="disabled" />

<activity
android:name=".ui.activities.MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,30 @@
package io.bitdrift.gradletestapp

import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import io.bitdrift.capture.Capture
import io.bitdrift.capture.Capture.Logger.sessionUrl
import io.bitdrift.capture.timber.CaptureTree
import io.bitdrift.gradletestapp.diagnostics.fatalissues.CrashSdkInitializer
import io.bitdrift.gradletestapp.diagnostics.lifecycle.ActivitySpanCallbacks
import io.bitdrift.gradletestapp.diagnostics.papa.PapaTelemetry
import io.bitdrift.gradletestapp.diagnostics.startup.AppStartInfoLogger
import io.bitdrift.gradletestapp.diagnostics.strictmode.StrictModeConfigurator
import io.bitdrift.gradletestapp.init.BitdriftInit
import io.bitdrift.gradletestapp.ui.fragments.ConfigurationSettingsFragment.Companion.DEFERRED_START_PREFS_KEY
import timber.log.Timber

/**
* A Kotlin app entry point that initializes the Bitdrift Logger automatically only when ConfigState.isDeferredStart is set to false
* App entry point. Capture SDK is auto-initialized via CaptureInitializer (manifest meta-data).
*/
class GradleTestApp : Application() {
private val shouldTriggerCrash = false
override fun onCreate() {
super.onCreate()

val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)

if (hasDeferredSdkStartConfigured(sharedPreferences)) {
Timber.i("Deferred start enabled - SDK initialization skipped")
} else {
BitdriftInit.init(this, sharedPreferences)
if (shouldTriggerCrash) {
throw IllegalStateException("Crash before Application super.onCreate()")
}

super.onCreate()
Timber.plant(CaptureTree())
Timber.i("Bitdrift auto-initialized with session_url=$sessionUrl")
attachAdditionalMonitoringTools()
}

private fun hasDeferredSdkStartConfigured(sharedPreferences: SharedPreferences): Boolean =
sharedPreferences.getBoolean(DEFERRED_START_PREFS_KEY, false)

private fun attachAdditionalMonitoringTools() {
StrictModeConfigurator.install()
CrashSdkInitializer.init(this)
Expand Down
Loading