From 49229acb877e9572f16038e54120edf52af8ad9f Mon Sep 17 00:00:00 2001 From: qingzhuozhen <84748495+qingzhuozhen@users.noreply.github.com> Date: Fri, 18 Mar 2022 23:50:34 -0700 Subject: [PATCH] feat: add event bridge module (#12) --- .github/workflows/pull-request-test.yml | 6 +- .../java/com/amplitude/android/Amplitude.kt | 2 +- .../com/amplitude/android/Configuration.kt | 3 +- .../android/utilities/AndroidStorage.kt | 3 +- build.gradle | 4 + .../common/android/AndroidContextProvider.kt | 32 ++++---- .../amplitude/common/android/AndroidLogger.kt | 9 +-- .../com/amplitude/common/android/Utils.kt | 5 +- .../com/amplitude/common/ContextProvider.kt | 3 +- core/build.gradle | 1 + .../main/java/com/amplitude/core/Amplitude.kt | 23 +++--- .../main/java/com/amplitude/core/Constants.kt | 2 +- .../java/com/amplitude/core/LoggerProvider.kt | 2 +- .../com/amplitude/core/events/BaseEvent.kt | 2 +- .../core/events/GroupIdentifyEvent.kt | 3 +- .../com/amplitude/core/events/Identify.kt | 4 +- .../amplitude/core/events/IdentifyEvent.kt | 3 +- .../java/com/amplitude/core/events/Plan.kt | 2 +- .../com/amplitude/core/events/RevenueEvent.kt | 2 +- .../amplitude/core/platform/EventPipeline.kt | 6 +- .../com/amplitude/core/platform/Plugin.kt | 4 +- .../platform/plugins/AmplitudeDestination.kt | 3 +- .../platform/plugins/IdentityEventSender.kt | 38 +++++++++ .../core/utilities/AnalyticsEventReceiver.kt | 24 ++++++ .../amplitude/core/utilities/FileStorage.kt | 4 +- .../amplitude/core/utilities/HttpClient.kt | 16 ++-- .../core/utilities/InMemoryStorage.kt | 3 +- .../java/com/amplitude/core/utilities/JSON.kt | 66 ++++++++++++++++ .../com/amplitude/core/utilities/JSONUtil.kt | 9 ++- .../com/amplitude/core/AmplitudeTest.kt | 4 +- .../com/amplitude/core/ConfigurationTest.kt | 4 +- event-bridge/build.gradle | 9 +++ .../com/amplitude/eventbridge/EventBridge.kt | 79 +++++++++++++++++++ .../eventbridge/EventBridgeContainer.kt | 21 +++++ .../com/amplitude/id/FileIdentityStorage.kt | 2 +- .../com/amplitude/id/IMIdentityStorage.kt | 1 - .../java/com/amplitude/id/IdentityManager.kt | 2 +- .../java/com/amplitude/id/IdentityStorage.kt | 2 +- .../amplitude/id/utilities/PropertiesFile.kt | 5 +- .../kotlin-jvm-app/src/main/kotlin/main.kt | 4 +- settings.gradle | 2 + 41 files changed, 332 insertions(+), 87 deletions(-) create mode 100644 core/src/main/java/com/amplitude/core/platform/plugins/IdentityEventSender.kt create mode 100644 core/src/main/java/com/amplitude/core/utilities/AnalyticsEventReceiver.kt create mode 100644 core/src/main/java/com/amplitude/core/utilities/JSON.kt create mode 100644 event-bridge/build.gradle create mode 100644 event-bridge/src/main/java/com/amplitude/eventbridge/EventBridge.kt create mode 100644 event-bridge/src/main/java/com/amplitude/eventbridge/EventBridgeContainer.kt diff --git a/.github/workflows/pull-request-test.yml b/.github/workflows/pull-request-test.yml index 7d9d8130..5187f704 100644 --- a/.github/workflows/pull-request-test.yml +++ b/.github/workflows/pull-request-test.yml @@ -1,4 +1,4 @@ -name: Test +name: Test and Lint on: [pull_request] @@ -13,4 +13,6 @@ jobs: distribution: 'zulu' - uses: actions/checkout@v2 - name: Unit Test - run: ./gradlew testDebugUnitTest \ No newline at end of file + run: ./gradlew testDebugUnitTest + - name: Lint + run: ./gradlew ktlintCheck diff --git a/android/src/main/java/com/amplitude/android/Amplitude.kt b/android/src/main/java/com/amplitude/android/Amplitude.kt index 2a540df7..e2c58303 100644 --- a/android/src/main/java/com/amplitude/android/Amplitude.kt +++ b/android/src/main/java/com/amplitude/android/Amplitude.kt @@ -9,7 +9,7 @@ import com.amplitude.id.IdentityContainer open class Amplitude( configuration: Configuration -): Amplitude(configuration) { +) : Amplitude(configuration) { override fun build() { idContainer = IdentityContainer.getInstance(IdentityConfiguration(instanceName = configuration.instanceName, apiKey = configuration.apiKey, identityStorageProvider = FileIdentityStorageProvider())) diff --git a/android/src/main/java/com/amplitude/android/Configuration.kt b/android/src/main/java/com/amplitude/android/Configuration.kt index 54766a79..5f1cbe98 100644 --- a/android/src/main/java/com/amplitude/android/Configuration.kt +++ b/android/src/main/java/com/amplitude/android/Configuration.kt @@ -1,11 +1,10 @@ package com.amplitude.android import android.content.Context +import com.amplitude.android.utilities.AndroidLoggerProvider import com.amplitude.core.Configuration import com.amplitude.core.LoggerProvider import com.amplitude.core.StorageProvider -import com.amplitude.android.utilities.AndroidLoggerProvider -import com.amplitude.android.utilities.AndroidStorageProvider import com.amplitude.core.events.BaseEvent import com.amplitude.core.utilities.FileStorageProvider diff --git a/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt b/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt index dd48612d..d503ccf7 100644 --- a/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt +++ b/android/src/main/java/com/amplitude/android/utilities/AndroidStorage.kt @@ -4,7 +4,6 @@ import com.amplitude.core.Amplitude import com.amplitude.core.Storage import com.amplitude.core.StorageProvider import com.amplitude.core.events.BaseEvent -import com.amplitude.core.utilities.FileStorageProvider class AndroidStorage( val amplitude: Amplitude @@ -30,7 +29,7 @@ class AndroidStorage( } } -class AndroidStorageProvider: StorageProvider { +class AndroidStorageProvider : StorageProvider { override fun getStorage(amplitude: Amplitude): Storage { return AndroidStorage(amplitude) } diff --git a/build.gradle b/build.gradle index a59442d8..0666f2d7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ buildscript { ext.kotlin_version = "1.5.10" repositories { + maven { url "https://plugins.gradle.org/m2/" } mavenCentral() google() } @@ -8,6 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" } } @@ -25,4 +27,6 @@ allprojects{ jvmTarget = "1.8" } } + + apply plugin: "org.jlleitschuh.gradle.ktlint" } \ No newline at end of file diff --git a/common-android/src/main/java/com/amplitude/common/android/AndroidContextProvider.kt b/common-android/src/main/java/com/amplitude/common/android/AndroidContextProvider.kt index 862bff75..ba8a5541 100644 --- a/common-android/src/main/java/com/amplitude/common/android/AndroidContextProvider.kt +++ b/common-android/src/main/java/com/amplitude/common/android/AndroidContextProvider.kt @@ -9,14 +9,16 @@ import android.location.LocationManager import android.os.Build import android.provider.Settings.Secure import android.telephony.TelephonyManager +import com.amplitude.common.ContextProvider import java.io.IOException import java.lang.Exception import java.lang.IllegalArgumentException import java.lang.IllegalStateException import java.lang.NullPointerException import java.lang.reflect.InvocationTargetException -import java.util.* -import com.amplitude.common.ContextProvider +import java.util.Locale +import java.util.UUID +import kotlin.collections.ArrayList class AndroidContextProvider(private val context: Context, locationListening: Boolean) : ContextProvider { @@ -48,18 +50,18 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo var appSetId: String init { - advertisingId = fetchAdvertisingId(); - versionName = fetchVersionName(); + advertisingId = fetchAdvertisingId() + versionName = fetchVersionName() osName = OS_NAME - osVersion = fetchOsVersion(); - brand = fetchBrand(); - manufacturer = fetchManufacturer(); - model = fetchModel(); - carrier = fetchCarrier(); - country = fetchCountry(); - language = fetchLanguage(); - gpsEnabled = checkGPSEnabled(); - appSetId = fetchAppSetId(); + osVersion = fetchOsVersion() + brand = fetchBrand() + manufacturer = fetchManufacturer() + model = fetchModel() + carrier = fetchCarrier() + country = fetchCountry() + language = fetchLanguage() + gpsEnabled = checkGPSEnabled() + appSetId = fetchAppSetId() } /** @@ -116,7 +118,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo return if (!country.isNullOrEmpty()) { country } else countryFromLocale - }// Customized Android System without Google Play Service Installed// sometimes the location manager is unavailable// Bad lat / lon values can cause Geocoder to throw IllegalArgumentExceptions// failed to fetch geocoder// Failed to reverse geocode location + } // Customized Android System without Google Play Service Installed// sometimes the location manager is unavailable// Bad lat / lon values can cause Geocoder to throw IllegalArgumentExceptions// failed to fetch geocoder// Failed to reverse geocode location // Failed to reverse geocode location private val countryFromLocation: String? @@ -323,7 +325,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo val advertisingId: String get() = cachedInfo!!.advertisingId val appSetId: String - get() = cachedInfo!!.appSetId// other causes// failed to get providers list + get() = cachedInfo!!.appSetId // other causes// failed to get providers list // Don't crash if the device does not have location services. // It's possible that the location service is running out of process diff --git a/common-android/src/main/java/com/amplitude/common/android/AndroidLogger.kt b/common-android/src/main/java/com/amplitude/common/android/AndroidLogger.kt index 6530ac2f..d71c36e7 100644 --- a/common-android/src/main/java/com/amplitude/common/android/AndroidLogger.kt +++ b/common-android/src/main/java/com/amplitude/common/android/AndroidLogger.kt @@ -3,15 +3,14 @@ package com.amplitude.common.android import android.util.Log import com.amplitude.common.Logger -class AndroidLogger(): Logger -{ +class AndroidLogger() : Logger { override var logMode: Logger.LogMode = Logger.LogMode.INFO private val tag = "Amplitude" override fun debug(message: String) { - if (logMode <= Logger.LogMode.DEBUG) { - Log.d(tag, message) - } + if (logMode <= Logger.LogMode.DEBUG) { + Log.d(tag, message) + } } override fun error(message: String) { diff --git a/common-android/src/main/java/com/amplitude/common/android/Utils.kt b/common-android/src/main/java/com/amplitude/common/android/Utils.kt index 432a37f0..14ddf473 100644 --- a/common-android/src/main/java/com/amplitude/common/android/Utils.kt +++ b/common-android/src/main/java/com/amplitude/common/android/Utils.kt @@ -1,10 +1,9 @@ package com.amplitude.common.android import android.Manifest -import android.content.pm.PackageManager - import android.app.Activity import android.content.Context +import android.content.pm.PackageManager import android.os.Build import java.lang.Exception @@ -12,7 +11,7 @@ object Utils { fun checkLocationPermissionAllowed(context: Context?): Boolean { return checkPermissionAllowed(context, Manifest.permission.ACCESS_COARSE_LOCATION) || - checkPermissionAllowed(context, Manifest.permission.ACCESS_FINE_LOCATION) + checkPermissionAllowed(context, Manifest.permission.ACCESS_FINE_LOCATION) } fun checkPermissionAllowed(context: Context?, permission: String?): Boolean { diff --git a/common/src/main/java/com/amplitude/common/ContextProvider.kt b/common/src/main/java/com/amplitude/common/ContextProvider.kt index 0b1c02a3..40d0bf3e 100644 --- a/common/src/main/java/com/amplitude/common/ContextProvider.kt +++ b/common/src/main/java/com/amplitude/common/ContextProvider.kt @@ -1,4 +1,3 @@ package com.amplitude.common -interface ContextProvider { -} \ No newline at end of file +interface ContextProvider diff --git a/core/build.gradle b/core/build.gradle index b78a18ff..6406ceef 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -22,6 +22,7 @@ dependencies { // MAIN DEPS compileOnly 'org.json:json:20211205' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation project(":event-bridge") testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' diff --git a/core/src/main/java/com/amplitude/core/Amplitude.kt b/core/src/main/java/com/amplitude/core/Amplitude.kt index d96e5e49..259acdc4 100644 --- a/core/src/main/java/com/amplitude/core/Amplitude.kt +++ b/core/src/main/java/com/amplitude/core/Amplitude.kt @@ -10,11 +10,18 @@ import com.amplitude.core.platform.Plugin import com.amplitude.core.platform.Timeline import com.amplitude.core.platform.plugins.AmplitudeDestination import com.amplitude.core.platform.plugins.ContextPlugin +import com.amplitude.core.utilities.AnalyticsEventReceiver import com.amplitude.core.utilities.AnalyticsIdentityListener +import com.amplitude.eventbridge.EventBridgeContainer +import com.amplitude.eventbridge.EventChannel import com.amplitude.id.IMIdentityStorageProvider import com.amplitude.id.IdentityConfiguration import com.amplitude.id.IdentityContainer -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch import java.util.concurrent.Executors open class Amplitude internal constructor( @@ -24,7 +31,7 @@ open class Amplitude internal constructor( val amplitudeDispatcher: CoroutineDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher(), val networkIODispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(), val storageIODispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() -){ +) { internal val timeline: Timeline val storage: Storage @@ -47,11 +54,11 @@ open class Amplitude internal constructor( open fun build() { idContainer = IdentityContainer.getInstance(IdentityConfiguration(instanceName = configuration.instanceName, apiKey = configuration.apiKey, identityStorageProvider = IMIdentityStorageProvider())) idContainer.identityManager.addIdentityListener(AnalyticsIdentityListener(store)) + EventBridgeContainer.getInstance(configuration.instanceName).eventBridge.setEventReceiver(EventChannel.EVENT, AnalyticsEventReceiver(this)) add(ContextPlugin()) add(AmplitudeDestination()) - amplitudeScope.launch (amplitudeDispatcher) { - + amplitudeScope.launch(amplitudeDispatcher) { } } @@ -65,7 +72,6 @@ open class Amplitude internal constructor( } fun identify(identify: Identify) { - } fun identify(userId: String) { @@ -77,11 +83,9 @@ open class Amplitude internal constructor( } fun groupIdentify(identify: Identify) { - } fun setGroup(groupType: String, groupName: Array) { - } @Deprecated("Please use 'revenue' instead.", ReplaceWith("revenue")) @@ -113,7 +117,7 @@ open class Amplitude internal constructor( } } - fun add(plugin: Plugin) : Amplitude { + fun add(plugin: Plugin): Amplitude { when (plugin) { is ObservePlugin -> { this.store.add(plugin) @@ -138,6 +142,5 @@ open class Amplitude internal constructor( } fun flush() { - } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/Constants.kt b/core/src/main/java/com/amplitude/core/Constants.kt index babbb448..fd92badc 100644 --- a/core/src/main/java/com/amplitude/core/Constants.kt +++ b/core/src/main/java/com/amplitude/core/Constants.kt @@ -10,4 +10,4 @@ object Constants { const val AMP_REVENUE_EVENT = "revenue_amount" const val MAX_PROPERTY_KEYS = 1024 const val MAX_STRING_LENGTH = 1024 -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/LoggerProvider.kt b/core/src/main/java/com/amplitude/core/LoggerProvider.kt index b0972c1f..c3b6cc10 100644 --- a/core/src/main/java/com/amplitude/core/LoggerProvider.kt +++ b/core/src/main/java/com/amplitude/core/LoggerProvider.kt @@ -4,4 +4,4 @@ import com.amplitude.common.Logger interface LoggerProvider { fun getLogger(amplitude: Amplitude): Logger -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/events/BaseEvent.kt b/core/src/main/java/com/amplitude/core/events/BaseEvent.kt index a28817cf..d4ad192c 100644 --- a/core/src/main/java/com/amplitude/core/events/BaseEvent.kt +++ b/core/src/main/java/com/amplitude/core/events/BaseEvent.kt @@ -6,6 +6,6 @@ open class BaseEvent : EventOptions() { open lateinit var eventType: String var eventProperties: JSONObject? = null var userProperties: JSONObject? = null - var groups:JSONObject? = null + var groups: JSONObject? = null var groupProperties: JSONObject? = null } diff --git a/core/src/main/java/com/amplitude/core/events/GroupIdentifyEvent.kt b/core/src/main/java/com/amplitude/core/events/GroupIdentifyEvent.kt index 573a3821..eb8b8525 100644 --- a/core/src/main/java/com/amplitude/core/events/GroupIdentifyEvent.kt +++ b/core/src/main/java/com/amplitude/core/events/GroupIdentifyEvent.kt @@ -1,4 +1,3 @@ package com.amplitude.core.events -class GroupIdentifyEvent : BaseEvent() { -} +class GroupIdentifyEvent : BaseEvent() diff --git a/core/src/main/java/com/amplitude/core/events/Identify.kt b/core/src/main/java/com/amplitude/core/events/Identify.kt index 7b4a0c68..9d3de955 100644 --- a/core/src/main/java/com/amplitude/core/events/Identify.kt +++ b/core/src/main/java/com/amplitude/core/events/Identify.kt @@ -19,10 +19,8 @@ class Identify() { internal val properties: MutableMap = mutableMapOf() init { - } internal fun setUserProperty(operation: IdentifyOperation, property: String, value: Any) { - } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/events/IdentifyEvent.kt b/core/src/main/java/com/amplitude/core/events/IdentifyEvent.kt index f9fe2308..45881c42 100644 --- a/core/src/main/java/com/amplitude/core/events/IdentifyEvent.kt +++ b/core/src/main/java/com/amplitude/core/events/IdentifyEvent.kt @@ -1,4 +1,3 @@ package com.amplitude.core.events -class IdentifyEvent : BaseEvent() { -} +class IdentifyEvent : BaseEvent() diff --git a/core/src/main/java/com/amplitude/core/events/Plan.kt b/core/src/main/java/com/amplitude/core/events/Plan.kt index 54ff7f65..4e86e9e2 100644 --- a/core/src/main/java/com/amplitude/core/events/Plan.kt +++ b/core/src/main/java/com/amplitude/core/events/Plan.kt @@ -4,4 +4,4 @@ open class Plan( var branch: String?, var source: String?, var version: String? -) \ No newline at end of file +) diff --git a/core/src/main/java/com/amplitude/core/events/RevenueEvent.kt b/core/src/main/java/com/amplitude/core/events/RevenueEvent.kt index 796dbe19..75ef27c6 100644 --- a/core/src/main/java/com/amplitude/core/events/RevenueEvent.kt +++ b/core/src/main/java/com/amplitude/core/events/RevenueEvent.kt @@ -4,4 +4,4 @@ import com.amplitude.core.Constants class RevenueEvent : BaseEvent() { override var eventType = Constants.AMP_REVENUE_EVENT -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt index 96b3099a..c27bd41a 100644 --- a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt +++ b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt @@ -3,10 +3,13 @@ package com.amplitude.core.platform import com.amplitude.core.Amplitude import com.amplitude.core.events.BaseEvent import com.amplitude.core.utilities.HttpClient -import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.lang.Exception import java.util.concurrent.atomic.AtomicInteger @@ -106,7 +109,6 @@ internal class EventPipeline( } } catch (e: Exception) { e.printStackTrace() - } // @TODO: handle failures and retry } diff --git a/core/src/main/java/com/amplitude/core/platform/Plugin.kt b/core/src/main/java/com/amplitude/core/platform/Plugin.kt index bbeeee89..2178103f 100644 --- a/core/src/main/java/com/amplitude/core/platform/Plugin.kt +++ b/core/src/main/java/com/amplitude/core/platform/Plugin.kt @@ -47,7 +47,7 @@ interface EventPlugin : Plugin { open fun flush() {} } -abstract class DestinationPlugin: EventPlugin { +abstract class DestinationPlugin : EventPlugin { override val type: Plugin.Type = Plugin.Type.Destination private val timeline: Timeline = Timeline() override lateinit var amplitude: Amplitude @@ -100,7 +100,7 @@ abstract class DestinationPlugin: EventPlugin { } } -abstract class ObservePlugin: Plugin { +abstract class ObservePlugin : Plugin { override val type: Plugin.Type = Plugin.Type.Observe abstract fun onUserIdChanged(userId: String?) diff --git a/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt b/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt index dabaf1e9..8b5b925d 100644 --- a/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt +++ b/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt @@ -50,5 +50,6 @@ class AmplitudeDestination : DestinationPlugin() { ) pipeline.start() } + add(IdentityEventSender()) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/platform/plugins/IdentityEventSender.kt b/core/src/main/java/com/amplitude/core/platform/plugins/IdentityEventSender.kt new file mode 100644 index 00000000..523797a9 --- /dev/null +++ b/core/src/main/java/com/amplitude/core/platform/plugins/IdentityEventSender.kt @@ -0,0 +1,38 @@ +package com.amplitude.core.platform.plugins + +import com.amplitude.core.Amplitude +import com.amplitude.core.events.BaseEvent +import com.amplitude.core.platform.Plugin +import com.amplitude.eventbridge.Event +import com.amplitude.eventbridge.EventBridge +import com.amplitude.eventbridge.EventBridgeContainer +import com.amplitude.eventbridge.EventChannel + +internal class IdentityEventSender : Plugin { + override val type: Plugin.Type = Plugin.Type.Before + override lateinit var amplitude: Amplitude + private lateinit var eventBridge: EventBridge + + override fun setup(amplitude: Amplitude) { + super.setup(amplitude) + eventBridge = + EventBridgeContainer.getInstance(amplitude.configuration.instanceName).eventBridge + } + + override fun execute(event: BaseEvent): BaseEvent? { + if (event.userProperties != null) { + eventBridge.sendEvent(EventChannel.IDENTIFY, event.toBridgeEvent()) + } + return event + } +} + +internal fun BaseEvent.toBridgeEvent(): Event { + return Event( + this.eventType, + this.eventProperties?.toMap(), + this.userProperties?.toMap(), + this.groups?.toMap(), + this.groupProperties?.toMap() + ) +} diff --git a/core/src/main/java/com/amplitude/core/utilities/AnalyticsEventReceiver.kt b/core/src/main/java/com/amplitude/core/utilities/AnalyticsEventReceiver.kt new file mode 100644 index 00000000..98b4c94b --- /dev/null +++ b/core/src/main/java/com/amplitude/core/utilities/AnalyticsEventReceiver.kt @@ -0,0 +1,24 @@ +package com.amplitude.core.utilities + +import com.amplitude.core.Amplitude +import com.amplitude.core.events.BaseEvent +import com.amplitude.eventbridge.Event +import com.amplitude.eventbridge.EventChannel +import com.amplitude.eventbridge.EventReceiver + +internal class AnalyticsEventReceiver(val amplitude: Amplitude) : EventReceiver { + override fun receive(channel: EventChannel, event: Event) { + amplitude.logger.debug("Receive event from event bridge ${event.eventType}") + amplitude.track(event.toBaseEvent()) + } +} + +internal fun Event.toBaseEvent(): BaseEvent { + val event = BaseEvent() + event.eventType = this.eventType + event.eventProperties = this.eventProperties.toJSONObject() + event.userProperties = this.userProperties.toJSONObject() + event.groups = this.groups.toJSONObject() + event.groupProperties = this.groupProperties.toJSONObject() + return event +} diff --git a/core/src/main/java/com/amplitude/core/utilities/FileStorage.kt b/core/src/main/java/com/amplitude/core/utilities/FileStorage.kt index a36eedfa..ed4c245e 100644 --- a/core/src/main/java/com/amplitude/core/utilities/FileStorage.kt +++ b/core/src/main/java/com/amplitude/core/utilities/FileStorage.kt @@ -55,8 +55,8 @@ class FileStorage( } } -class FileStorageProvider: StorageProvider { +class FileStorageProvider : StorageProvider { override fun getStorage(amplitude: Amplitude): Storage { return FileStorage(amplitude.configuration.apiKey) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/utilities/HttpClient.kt b/core/src/main/java/com/amplitude/core/utilities/HttpClient.kt index 7628e401..bcbb1ace 100644 --- a/core/src/main/java/com/amplitude/core/utilities/HttpClient.kt +++ b/core/src/main/java/com/amplitude/core/utilities/HttpClient.kt @@ -2,7 +2,11 @@ package com.amplitude.core.utilities import com.amplitude.core.Configuration import com.amplitude.core.Constants -import java.io.* +import java.io.BufferedReader +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import java.net.HttpURLConnection import java.net.MalformedURLException import java.net.URL @@ -32,8 +36,10 @@ internal class HttpClient( inputStream = getInputStream(this.connection) responseBody = inputStream?.bufferedReader()?.use(BufferedReader::readText) } catch (e: IOException) { - responseBody = ("Could not read response body for rejected message: " - + e.toString()) + responseBody = ( + "Could not read response body for rejected message: " + + e.toString() + ) } finally { inputStream?.close() } @@ -76,7 +82,6 @@ internal class HttpClient( return configuration.minIdLength } - fun getInputStream(connection: HttpURLConnection): InputStream { return try { connection.inputStream @@ -84,7 +89,6 @@ internal class HttpClient( connection.errorStream } } - } abstract class Connection( @@ -137,4 +141,4 @@ internal enum class HttpStatus(val code: Int) { PAYLOAD_TOO_LARGE(413), TOO_MANY_REQUESTS(429), FAILED(500) -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt b/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt index bcedc2de..207d77e5 100644 --- a/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt +++ b/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt @@ -25,7 +25,6 @@ class InMemoryStorage( } override suspend fun rollover() { - } override fun read(key: Storage.Constants): String? { @@ -46,4 +45,4 @@ class InMemoryStorageProvider : StorageProvider { override fun getStorage(amplitude: Amplitude): Storage { return InMemoryStorage(amplitude) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplitude/core/utilities/JSON.kt b/core/src/main/java/com/amplitude/core/utilities/JSON.kt new file mode 100644 index 00000000..716931f6 --- /dev/null +++ b/core/src/main/java/com/amplitude/core/utilities/JSON.kt @@ -0,0 +1,66 @@ +package com.amplitude.core.utilities + +import org.json.JSONArray +import org.json.JSONObject +import java.math.BigDecimal + +internal fun Map<*, *>?.toJSONObject(): JSONObject? { + if (this == null) { + return null + } + val jsonObject = JSONObject() + for (entry in entries) { + val key = entry.key as? String ?: continue + val value = entry.value.toJSON() + jsonObject.put(key, value) + } + return jsonObject +} + +internal fun JSONObject.toMap(): Map { + val map = mutableMapOf() + for (key in this.keys()) { + map[key] = this[key].fromJSON() + } + return map +} + +internal fun JSONArray.toList(): List { + val list = mutableListOf() + for (i in 0 until this.length()) { + val value = this[i].fromJSON() + list.add(value) + } + return list +} + +internal fun List<*>?.toJSONArray(): JSONArray? { + if (this == null) { + return null + } + val jsonArray = JSONArray() + for (element in this) { + jsonArray.put(element.toJSON()) + } + return jsonArray +} + +private fun Any?.fromJSON(): Any? { + return when (this) { + is JSONObject -> this.toMap() + is JSONArray -> this.toList() + // org.json uses BigDecimal for doubles and floats; normalize to double + // to make testing for equality easier. + is BigDecimal -> this.toDouble() + JSONObject.NULL -> null + else -> this + } +} + +private fun Any?.toJSON(): Any? { + return when (this) { + is Map<*, *> -> this.toJSONObject() + is List<*> -> this.toJSONArray() + else -> this + } +} diff --git a/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt b/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt index 3b52645b..42c352e8 100644 --- a/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt +++ b/core/src/main/java/com/amplitude/core/utilities/JSONUtil.kt @@ -89,9 +89,10 @@ object JSONUtil { } } catch (e: JSONException) { throw IllegalArgumentException( - ("JSON parsing error. Too long (>" - + Constants.MAX_STRING_LENGTH - ) + " chars) or invalid JSON" + ( + "JSON parsing error. Too long (>" + + Constants.MAX_STRING_LENGTH + ) + " chars) or invalid JSON" ) } } @@ -122,4 +123,4 @@ object JSONUtil { Constants.MAX_STRING_LENGTH ) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/com/amplitude/core/AmplitudeTest.kt b/core/src/test/kotlin/com/amplitude/core/AmplitudeTest.kt index f60fb8ad..9cce726e 100644 --- a/core/src/test/kotlin/com/amplitude/core/AmplitudeTest.kt +++ b/core/src/test/kotlin/com/amplitude/core/AmplitudeTest.kt @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test internal class AmplitudeTest { @Test - fun `test place holder` () { + fun `test place holder`() { assertTrue(true) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/com/amplitude/core/ConfigurationTest.kt b/core/src/test/kotlin/com/amplitude/core/ConfigurationTest.kt index f06d3661..c9fe584f 100644 --- a/core/src/test/kotlin/com/amplitude/core/ConfigurationTest.kt +++ b/core/src/test/kotlin/com/amplitude/core/ConfigurationTest.kt @@ -2,11 +2,9 @@ package com.amplitude.core import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions.* - internal class ConfigurationTest { @Test fun isValid() { } -} \ No newline at end of file +} diff --git a/event-bridge/build.gradle b/event-bridge/build.gradle new file mode 100644 index 00000000..a1a673c9 --- /dev/null +++ b/event-bridge/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java-library' + id 'kotlin' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} \ No newline at end of file diff --git a/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridge.kt b/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridge.kt new file mode 100644 index 00000000..35a829fe --- /dev/null +++ b/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridge.kt @@ -0,0 +1,79 @@ +package com.amplitude.eventbridge + +import java.util.concurrent.ArrayBlockingQueue + +data class Event( + val eventType: String, + val eventProperties: Map? = null, + val userProperties: Map? = null, + val groups: Map? = null, + var groupProperties: Map? = null +) + +enum class EventChannel { + EVENT, IDENTIFY +} + +interface EventReceiver { + fun receive(channel: EventChannel, event: Event) +} + +interface EventBridge { + fun sendEvent(channel: EventChannel, event: Event) + fun setEventReceiver(channel: EventChannel, receiver: EventReceiver) +} + +internal class EventBridgeImpl : EventBridge { + private val lock = Any() + private val channels = mutableMapOf() + + override fun sendEvent(channel: EventChannel, event: Event) { + synchronized(lock) { + channels.getOrPut(channel) { + EventBridgeChannel(channel) + } + }.sendEvent(event) + } + + override fun setEventReceiver(channel: EventChannel, receiver: EventReceiver) { + synchronized(lock) { + channels.getOrPut(channel) { + EventBridgeChannel(channel) + } + }.setEventReceiver(receiver) + } +} + +internal class EventBridgeChannel(private val channel: EventChannel) { + companion object { + const val QUEUE_CAPACITY = 512 + } + + private val lock = Any() + private var receiver: EventReceiver? = null + private val queue = ArrayBlockingQueue(QUEUE_CAPACITY) + + fun sendEvent(event: Event) { + synchronized(lock) { + if (this.receiver == null) { + queue.offer(event) + } + this.receiver + }?.receive(channel, event) + } + + fun setEventReceiver(receiver: EventReceiver?) { + synchronized(lock) { + if (this.receiver != null) { + // Only allow one receiver per channel now + return + } + this.receiver = receiver + mutableListOf().apply { + queue.drainTo(this) + } + }.forEach { event -> + receiver?.receive(channel, event) + } + } +} diff --git a/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridgeContainer.kt b/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridgeContainer.kt new file mode 100644 index 00000000..b7ccd7c2 --- /dev/null +++ b/event-bridge/src/main/java/com/amplitude/eventbridge/EventBridgeContainer.kt @@ -0,0 +1,21 @@ +package com.amplitude.eventbridge + +class EventBridgeContainer { + + companion object { + + private val instancesLock = Any() + private val instances = mutableMapOf() + + @JvmStatic + fun getInstance(instanceName: String): EventBridgeContainer { + return synchronized(instancesLock) { + instances.getOrPut(instanceName) { + EventBridgeContainer() + } + } + } + } + + val eventBridge: EventBridge = EventBridgeImpl() +} diff --git a/id/src/main/java/com/amplitude/id/FileIdentityStorage.kt b/id/src/main/java/com/amplitude/id/FileIdentityStorage.kt index b8ca13bc..f5e4643d 100644 --- a/id/src/main/java/com/amplitude/id/FileIdentityStorage.kt +++ b/id/src/main/java/com/amplitude/id/FileIdentityStorage.kt @@ -68,4 +68,4 @@ class FileIdentityStorageProvider : IdentityStorageProvider { override fun getIdentityStorage(configuration: IdentityConfiguration): IdentityStorage { return FileIdentityStorage(configuration) } -} \ No newline at end of file +} diff --git a/id/src/main/java/com/amplitude/id/IMIdentityStorage.kt b/id/src/main/java/com/amplitude/id/IMIdentityStorage.kt index d8b292eb..6744d0f5 100644 --- a/id/src/main/java/com/amplitude/id/IMIdentityStorage.kt +++ b/id/src/main/java/com/amplitude/id/IMIdentityStorage.kt @@ -4,7 +4,6 @@ class IMIdentityStorage : IdentityStorage { var userId: String? = null var deviceId: String? = null - override fun load(): Identity { return Identity(userId, deviceId) } diff --git a/id/src/main/java/com/amplitude/id/IdentityManager.kt b/id/src/main/java/com/amplitude/id/IdentityManager.kt index 11c1810f..6e53e9e7 100644 --- a/id/src/main/java/com/amplitude/id/IdentityManager.kt +++ b/id/src/main/java/com/amplitude/id/IdentityManager.kt @@ -39,7 +39,7 @@ interface IdentityManager { fun isInitialized(): Boolean } -internal class IdentityManagerImpl(private val identityStorage: IdentityStorage): IdentityManager { +internal class IdentityManagerImpl(private val identityStorage: IdentityStorage) : IdentityManager { private val identityLock = ReentrantReadWriteLock(true) private var identity = Identity() diff --git a/id/src/main/java/com/amplitude/id/IdentityStorage.kt b/id/src/main/java/com/amplitude/id/IdentityStorage.kt index 81ea93fd..f4af8542 100644 --- a/id/src/main/java/com/amplitude/id/IdentityStorage.kt +++ b/id/src/main/java/com/amplitude/id/IdentityStorage.kt @@ -2,7 +2,7 @@ package com.amplitude.id interface IdentityStorage { - fun load():Identity + fun load(): Identity fun saveUserId(userId: String?) diff --git a/id/src/main/java/com/amplitude/id/utilities/PropertiesFile.kt b/id/src/main/java/com/amplitude/id/utilities/PropertiesFile.kt index e93c3dcc..f7e0a355 100644 --- a/id/src/main/java/com/amplitude/id/utilities/PropertiesFile.kt +++ b/id/src/main/java/com/amplitude/id/utilities/PropertiesFile.kt @@ -3,7 +3,7 @@ package com.amplitude.id.utilities import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -import java.util.* +import java.util.Properties class PropertiesFile(private val directory: File, key: String, prefix: String) : KeyValueStore { private val underlyingProperties: Properties = Properties() @@ -16,8 +16,7 @@ class PropertiesFile(private val directory: File, key: String, prefix: String) : fun load() { if (propertiesFile.exists()) { underlyingProperties.load(FileInputStream(propertiesFile)) - } - else { + } else { propertiesFile.parentFile.mkdirs() propertiesFile.createNewFile() } diff --git a/samples/kotlin-jvm-app/src/main/kotlin/main.kt b/samples/kotlin-jvm-app/src/main/kotlin/main.kt index cc3ae8e0..e7d381ab 100644 --- a/samples/kotlin-jvm-app/src/main/kotlin/main.kt +++ b/samples/kotlin-jvm-app/src/main/kotlin/main.kt @@ -8,12 +8,12 @@ fun main() { runBlocking { val amplitude = Amplitude( Configuration( - apiKey = "bbdf6e7b53f5d0e48b40f2eb51cd8ab4", + apiKey = "API-Key", storageProvider = FileStorageProvider() ) ) val event = BaseEvent() - event.eventType = "Kotlin JVM Test with retry" + event.eventType = "Kotlin JVM Test" event.userId = "kotlin-test-user" event.deviceId = "kotlin-test-device" amplitude.track(event) diff --git a/settings.gradle b/settings.gradle index ced56d8c..6f6ab2b1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,8 @@ include 'core' project(':core').projectDir = file('core') include 'samples:kotlin-jvm-app' project(':samples:kotlin-jvm-app').projectDir = file('samples/kotlin-jvm-app') +include 'event-bridge' +project(':event-bridge').projectDir = file('event-bridge') include 'android' project(':android').projectDir = file('android') include 'id'