Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: remove IntegrationState and simplify IntegrationPlugin #113

Draft
wants to merge 74 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
09da8cb
feat: add DeviceModeDestinationPlugin and StartupQueuePlugin
Dec 26, 2024
5f0c076
refactor: change StartupQueuePlugin to use HashSet
Dec 27, 2024
fea280f
feat: add DestinationPlugin
Dec 27, 2024
924e0ab
refactor: remove StartupQueue and move logic to DeviceModeDestination…
Dec 27, 2024
3a8c72c
refactor: DestinationPlugin and DeviceModeDestinationPlugin
Dec 27, 2024
e7e92bd
feat: add EventProcessor and EventProcessorFacade
Dec 28, 2024
f0a508d
refactor: change sourceConfig logic in DeviceModeDestinationPlugin
Dec 28, 2024
4de710c
feat: add a sample amplitude destination plugin
Dec 28, 2024
3f03278
refactor: make DeviceModeDestinationPlugin nullable
Dec 30, 2024
62f1981
refactor: move reset and flush logic to DeviceModeDestinationPlugin
Dec 30, 2024
97d0d4b
refactor: extract out default event processor list
Dec 30, 2024
8386588
refactor: make public api methods as protected in DestinationPlugin
Jan 2, 2025
34337cc
refactor: use PluginChain and plugin architecture instead of EventPro…
Jan 2, 2025
05980fb
refactor: add EventFilteringPlugin and change add plugin logic for De…
Jan 2, 2025
f48470f
refactor: remove EventFilteringPlugin and IntegrationOptionsPlugin
Jan 4, 2025
8682327
refactor: make deviceModeDestinationPlugin initialisation synchronised
Jan 4, 2025
d6ac9a6
refactor: change initialize login in DestinationPlugin
Jan 4, 2025
86f9cee
refactor: change DestinationPlugin Apis logic to avoid unitialized ex…
Jan 4, 2025
84b7aa1
refactor: refactor DeviceModeDestinationPlugin
Jan 4, 2025
b6e5823
refactor: make amplitudeSdk in SampleAmplitudePlugin nullable
Jan 6, 2025
5d05a80
refactor: use EventPlugin in DestinationPlugin
Jan 6, 2025
7a3a60f
refactor: refactor create to return DestinationResult and add getUnde…
Jan 7, 2025
1f60a7c
refactor: change logic for DestinationPlugin and DeviceModeDestinatio…
Jan 9, 2025
ca2fbd2
refactor: make handleEvents method in EventPlugin
Jan 9, 2025
6b616e4
refactor: add internal modifiers in DeviceModeDestinationPlugin
Jan 9, 2025
c72024e
refactor: make pluginChain instance of Analytics class as private
Jan 9, 2025
3ba12b0
refactor: rename DestinationPlugin to IntegrationPlugin
Jan 10, 2025
2adb599
refactor: rename DeviceModeDestinationPlugin to IntegrationsManagemen…
Jan 10, 2025
590864b
refactor: add nullability in identify and alias apis in EventPlugin
Jan 10, 2025
3b00f38
refactor: rename methods in IntegrationsManagementPlugin
Jan 10, 2025
f15bcb8
refactor: make DestinationState a sealed interface
Jan 10, 2025
13202db
refactor: remove find from PluginChain
Jan 10, 2025
e3f307c
refactor: change findAll signature in PluginChain
Jan 10, 2025
23cc0a7
refactor: move DestinationState to a new file
Jan 10, 2025
9044a93
docs: add documentation for IntegrationPlugin
Jan 10, 2025
47816ec
docs: add docs for SdkNotInitializedException
Jan 10, 2025
152424d
refactor: change onDestinationReady signature to have plugin as the p…
Jan 10, 2025
75dace9
docs: add documentation for integration related APIs in Analytics class
Jan 10, 2025
db4dd30
refactor: rename methods in IntegrationsManagementPlugin
Jan 11, 2025
0295f38
test: add IntegrationPluginTest
Jan 11, 2025
b3a0a16
docs: add docs for PluginInteractor
Jan 11, 2025
482285d
docs: add docs for PluginChain
Jan 11, 2025
9be1aaa
docs: add docs for EventPlugin
Jan 11, 2025
11edc70
docs: add docs for SourceConfig
Jan 11, 2025
7071af3
test: add IntegrationsManagementPluginTest
Jan 12, 2025
b586820
test: update IntegrationsManagementPluginTest and IntegrationPluginTest
Jan 12, 2025
5fc158c
refactor: remove serilisation plugin from gradle
Jan 12, 2025
3b7451b
docs: add docs for DestinationResult
Jan 12, 2025
b5aff50
refactor: rename SampleAmplitudePlugin to SampleIntegrationPlugin
Jan 13, 2025
0aafb4e
refactor: move destination to Failed state when not found in sourceCo…
Jan 13, 2025
9081129
test: update IntegrationPluginTest to cover more scenarios
Jan 16, 2025
245db58
test: update IntegrationsManagementPluginTest
Jan 16, 2025
063dfcc
feat: add destination updation logic for an integration
Jan 16, 2025
b8f3c50
test: update IntegrationPluginTest to include update logic tests
Jan 16, 2025
26040da
refactor: change the logic for updation of a destination
Jan 17, 2025
4a96d91
test: update IntegrationsManagementPluginTest to include destination …
Jan 17, 2025
3d5758b
test: update IntegrationPluginTest with scenarios for dynamic update
Jan 17, 2025
7db3037
Merge branch 'develop' into feat/sdk-2861-implement-dynamic-destinati…
Jan 17, 2025
46aae52
refactor: change the sourceConfig collection code in IntegrationsMana…
Jan 17, 2025
79d6962
refactor: use safelyExecute method for create and update in Integrati…
Jan 17, 2025
5a2bfec
refactor: add Synchronized annotation for initAndNotifyCallbacks as i…
Jan 17, 2025
c4ca4a2
refactor: change getUnderlyingInstance name to getDestinationInstance
Jan 17, 2025
eb52acd
refactor: change signature for create method in IntegrationPlugin
Jan 19, 2025
129bf52
refactor: rename DestinationState to IntegrationState
Jan 19, 2025
c422853
refactor: update IntegrationPlugin
Jan 22, 2025
57d530a
Merge branch 'develop' into feat/sdk-2861-implement-dynamic-destinati…
Jan 22, 2025
59bc681
refactor: change IntegrationsManagementPlugin's ogic to handle first …
Jan 22, 2025
b388554
refactor: change update logic in SampleIntegrationPlugin
Jan 22, 2025
c9bf016
test: change update logic for MockDestinationIntegrationPlugin
Jan 22, 2025
20bc01d
test: update IntegrationPluginTest
Jan 22, 2025
3e10189
test: update MockDestinationSdk to not return spy object for detinati…
Jan 22, 2025
b16f8c5
test: add test for updation of destinationConfig
Jan 22, 2025
b5ec459
refactor: remove IntegrationState and simplify IntegrationPlugin
Jan 30, 2025
3659be0
refactor: add IntegrationsManagementPlugin by default in the PluginChain
Jan 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.rudderstack.sdk.kotlin.android.plugins.NetworkInfoPlugin
import com.rudderstack.sdk.kotlin.android.plugins.OSInfoPlugin
import com.rudderstack.sdk.kotlin.android.plugins.ScreenInfoPlugin
import com.rudderstack.sdk.kotlin.android.plugins.TimezoneInfoPlugin
import com.rudderstack.sdk.kotlin.android.plugins.devicemode.DestinationResult
import com.rudderstack.sdk.kotlin.android.plugins.devicemode.IntegrationPlugin
import com.rudderstack.sdk.kotlin.android.plugins.devicemode.IntegrationsManagementPlugin
import com.rudderstack.sdk.kotlin.android.plugins.lifecyclemanagment.ActivityLifecycleManagementPlugin
Expand Down Expand Up @@ -80,7 +79,7 @@ class Analytics(

internal val activityLifecycleManagementPlugin = ActivityLifecycleManagementPlugin()
internal val processLifecycleManagementPlugin = ProcessLifecycleManagementPlugin()
private var integrationsManagementPlugin: IntegrationsManagementPlugin? = null
private var integrationsManagementPlugin = IntegrationsManagementPlugin()
private val sessionTrackingPlugin = SessionTrackingPlugin()

init {
Expand Down Expand Up @@ -134,15 +133,15 @@ class Analytics(
super.reset(clearAnonymousId)

sessionTrackingPlugin.refreshSession()
this.integrationsManagementPlugin?.reset()
integrationsManagementPlugin.reset()
}

override fun flush() {
if (!isAnalyticsActive()) return

super.flush()

this.integrationsManagementPlugin?.flush()
integrationsManagementPlugin.flush()
}

/**
Expand Down Expand Up @@ -227,9 +226,7 @@ class Analytics(
fun addIntegration(plugin: IntegrationPlugin) {
if (!isAnalyticsActive()) return

initIntegrationsManagementPlugin()

integrationsManagementPlugin?.addIntegration(plugin)
integrationsManagementPlugin.addIntegration(plugin)
}

/**
Expand All @@ -240,29 +237,7 @@ class Analytics(
fun removeIntegration(plugin: IntegrationPlugin) {
if (!isAnalyticsActive()) return

integrationsManagementPlugin?.removeIntegration(plugin)
}

/**
* Registers a callback to be invoked when the destination of the [plugin] is ready.
*
* @param plugin The [IntegrationPlugin] for which the callback needs to be invoked.
* @param onReady The callback to be invoked when the destination is ready.
*/
fun onDestinationReady(plugin: IntegrationPlugin, onReady: (Any?, DestinationResult) -> Unit) {
if (!isAnalyticsActive()) return

initIntegrationsManagementPlugin()

integrationsManagementPlugin?.onDestinationReady(plugin, onReady)
}

private fun initIntegrationsManagementPlugin() {
synchronized(this) {
if (integrationsManagementPlugin == null) {
integrationsManagementPlugin = IntegrationsManagementPlugin().also { add(it) }
}
}
integrationsManagementPlugin.removeIntegration(plugin)
}

private fun setup() {
Expand All @@ -276,6 +251,7 @@ class Analytics(
add(ScreenInfoPlugin())
add(TimezoneInfoPlugin())
add(sessionTrackingPlugin)
add(integrationsManagementPlugin)

// Add these plugins at last in chain
add(AndroidLifecyclePlugin())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ import com.rudderstack.sdk.kotlin.core.internals.utils.Result
/**
* The result of a destination initialisation returned in `onDestinationReady` callback.
*/
typealias DestinationResult = Result<Unit, Exception>
typealias DestinationResult = Result<Unit, Throwable>
1abhishekpandey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.rudderstack.sdk.kotlin.android.plugins.devicemode

import com.rudderstack.sdk.kotlin.android.Configuration
import com.rudderstack.sdk.kotlin.core.Analytics
import com.rudderstack.sdk.kotlin.core.internals.logger.LoggerAnalytics
import com.rudderstack.sdk.kotlin.core.internals.models.Destination
import com.rudderstack.sdk.kotlin.core.internals.models.Event
import com.rudderstack.sdk.kotlin.core.internals.models.SourceConfig
import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject
import com.rudderstack.sdk.kotlin.core.internals.plugins.EventPlugin
import com.rudderstack.sdk.kotlin.core.internals.plugins.Plugin
import com.rudderstack.sdk.kotlin.core.internals.plugins.PluginChain
import com.rudderstack.sdk.kotlin.core.internals.utils.Result
import com.rudderstack.sdk.kotlin.core.internals.utils.safelyExecute
import kotlinx.serialization.json.JsonObject
import java.util.concurrent.CopyOnWriteArrayList

Expand All @@ -18,44 +20,51 @@ import java.util.concurrent.CopyOnWriteArrayList
* An integration plugin is a plugin that is responsible for sending events directly
* to a 3rd party destination without sending it to Rudder server first.
*/
@Suppress("TooGenericExceptionCaught")
@Suppress("TooManyFunctions")
abstract class IntegrationPlugin : EventPlugin {

final override val pluginType: Plugin.PluginType = Plugin.PluginType.Destination

final override lateinit var analytics: Analytics

private lateinit var pluginChain: PluginChain
private val pluginList = CopyOnWriteArrayList<Plugin>()
private val destinationReadyCallbacks = mutableListOf<(Any?, DestinationResult) -> Unit>()

@Volatile
private var isPluginSetup = false

@Volatile
internal var isDestinationReady = false
private set

/**
* The key for the destination present in the source config.
*/
abstract val key: String

/**
* The configuration for the destination.
* This variable always holds the latest configuration for the destination from [SourceConfig].
*/
@Volatile
internal var destinationState: DestinationState = DestinationState.Uninitialised
var destinationConfig: JsonObject = emptyJsonObject
private set

private lateinit var pluginChain: PluginChain
private val pluginList = CopyOnWriteArrayList<Plugin>()

/**
* Creates the destination instance. Override this method for the initialisation of destination.
* This method must return true if the destination was created successfully, false otherwise.
*
* @param destinationConfig The configuration for the destination.
* @param analytics The analytics instance.
* @param config The configuration instance.
* @return true if the destination was created successfully, false otherwise.
*/
protected abstract fun create(destinationConfig: JsonObject, analytics: Analytics, config: Configuration): Boolean
protected abstract fun create(destinationConfig: JsonObject)

/**
* Returns the instance of the destination which was created.
*
* @return The instance of the destination.
*/
open fun getUnderlyingInstance(): Any? {
return null
}
abstract fun getDestinationInstance(): Any?

/**
* Override this method to control the behaviour of [Analytics.flush] for this destination.
Expand All @@ -71,50 +80,31 @@ abstract class IntegrationPlugin : EventPlugin {
super.setup(analytics)

pluginChain = PluginChain().also { it.analytics = analytics }
isPluginSetup = true
applyDefaultPlugins()
applyCustomPlugins()
}

internal fun initialize(sourceConfig: SourceConfig) {
internal fun findAndInitDestination(sourceConfig: SourceConfig) {
findDestination(sourceConfig)?.let { configDestination ->
if (!configDestination.isDestinationEnabled) {
val errorMessage = "Destination $key is disabled in dashboard. No events will be sent to this destination."
LoggerAnalytics.warn(errorMessage)
destinationState = DestinationState.Failed(SdkNotInitializedException(errorMessage))
val errorMessage = "Destination $key is disabled in dashboard. " +
"No events will be sent to this destination."
LoggerAnalytics.warn("IntegrationPlugin: $errorMessage")
setFailureConfigAndNotifyCallbacks(IllegalStateException(errorMessage))
return
}

try {
when (
create(
configDestination.destinationConfig,
analytics,
analytics.configuration as Configuration
)
) {
true -> {
destinationState = DestinationState.Ready
LoggerAnalytics.debug("IntegrationPlugin: Destination $key is ready.")
applyDefaultPlugins()
applyCustomPlugins()
}
false -> {
val errorMessage = "Destination $key failed to initialise."
destinationState = DestinationState.Failed(SdkNotInitializedException(errorMessage))
LoggerAnalytics.warn("IntegrationPlugin: $errorMessage")
}
}
} catch (e: Exception) {
destinationState = DestinationState.Failed(e)
LoggerAnalytics.error("IntegrationPlugin: Error: ${e.message} initializing destination $key.")
}
createSafelyAndNotifyCallbacks(configDestination.destinationConfig)
} ?: run {
val errorMessage = "Destination $key not found in the source config. No events will be sent to this destination."
destinationState = DestinationState.Failed(SdkNotInitializedException(errorMessage))
val errorMessage = "Destination $key not found in the source config. " +
"No events will be sent to this destination."
LoggerAnalytics.warn("IntegrationPlugin: $errorMessage")
setFailureConfigAndNotifyCallbacks(IllegalStateException(errorMessage))
}
}

final override suspend fun intercept(event: Event): Event {
if (destinationState.isReady()) {
if (isDestinationReady) {
event.copy<Event>()
.let { pluginChain.applyPlugins(Plugin.PluginType.PreProcess, it) }
?.let { pluginChain.applyPlugins(Plugin.PluginType.OnProcess, it) }
Expand All @@ -130,10 +120,9 @@ abstract class IntegrationPlugin : EventPlugin {
* **Note**: Calling of `super.teardown()` is recommended when overriding this method.
*/
override fun teardown() {
if (destinationState.isReady()) {
pluginList.clear()
if (isPluginSetup) {
pluginChain.removeAll()
} else {
pluginList.clear()
}
}

Expand All @@ -143,7 +132,7 @@ abstract class IntegrationPlugin : EventPlugin {
* @param plugin The plugin to be added.
*/
fun add(plugin: Plugin) {
if (destinationState.isReady()) {
if (isPluginSetup) {
pluginChain.add(plugin)
} else {
pluginList.add(plugin)
Expand All @@ -156,10 +145,66 @@ abstract class IntegrationPlugin : EventPlugin {
* @param plugin The plugin to be removed.
*/
fun remove(plugin: Plugin) {
if (destinationState.isReady()) {
pluginList.remove(plugin)
if (isPluginSetup) {
pluginChain.remove(plugin)
} else {
pluginList.remove(plugin)
}
}

/**
* Registers a callback to be invoked when the destination of this plugin is ready.
*
* @param callback The callback to be invoked when the destination is ready.
*/
fun onDestinationReady(callback: (Any?, DestinationResult) -> Unit) {
getDestinationInstance()?.let { destinationInstance ->
if (isDestinationReady) {
callback(destinationInstance, Result.Success(Unit))
} else {
callback(
null,
Result.Failure(null, IllegalStateException("Destination $key is absent or disabled in dashboard."))
)
}
} ?: run {
synchronized(this) {
destinationReadyCallbacks.add(callback)
}
}
}

private fun createSafelyAndNotifyCallbacks(destinationConfig: JsonObject) {
safelyExecute(
block = {
if (getDestinationInstance() == null) {
create(destinationConfig)
}
LoggerAnalytics.debug("IntegrationPlugin: Destination $key created successfully.")
setSuccessConfigAndNotifyCallbacks(destinationConfig)
},
onException = { exception ->
LoggerAnalytics.error("IntegrationPlugin: Error: ${exception.message} initializing destination $key.")
setFailureConfigAndNotifyCallbacks(exception)
}
)
}

private fun setFailureConfigAndNotifyCallbacks(throwable: Throwable) {
this.destinationConfig = emptyJsonObject
this.isDestinationReady = false
notifyCallbacks(Result.Failure(null, throwable))
}

private fun setSuccessConfigAndNotifyCallbacks(destinationConfig: JsonObject) {
this.destinationConfig = destinationConfig
this.isDestinationReady = true
notifyCallbacks(Result.Success(Unit))
}

private fun notifyCallbacks(destinationResult: DestinationResult) {
synchronized(this) {
destinationReadyCallbacks.forEach { callback -> callback(getDestinationInstance(), destinationResult) }
destinationReadyCallbacks.clear()
}
}

Expand All @@ -176,13 +221,3 @@ abstract class IntegrationPlugin : EventPlugin {
return sourceConfig.source.destinations.firstOrNull { it.destinationDefinition.displayName == key }
}
}

internal sealed interface DestinationState {
data object Ready : DestinationState
data object Uninitialised : DestinationState
data class Failed(
val exception: Exception
) : DestinationState

fun isReady() = this == Ready
}
Loading