diff --git a/.github/workflows/build-v2.yml b/.github/workflows/build-v2.yml index 3f680c502..f49894a75 100644 --- a/.github/workflows/build-v2.yml +++ b/.github/workflows/build-v2.yml @@ -2,15 +2,14 @@ name: CI for v2 on: push: - branches: [ 'master-v2' ] + branches: ["master-v2"] pull_request: - branches: [ 'master-v2', 'develop-v2' ] - types: [ 'opened', 'reopened', 'edited', 'synchronize' ] + branches: ["master-v2", "develop-v2"] + types: ["opened", "reopened", "edited", "synchronize"] workflow_dispatch: jobs: cancel_previous-v2: - runs-on: ubuntu-latest steps: - uses: styfle/cancel-workflow-action@0.9.1 @@ -29,8 +28,8 @@ jobs: - name: Setup Java uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: '11' + distribution: "temurin" + java-version: "17" - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -38,8 +37,8 @@ jobs: - name: Set Node 16 uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' - cache: 'npm' + node-version-file: ".nvmrc" + cache: "npm" - name: Install node_modules run: | @@ -69,4 +68,4 @@ jobs: npm run test - name: Android Test Report uses: asadmansr/android-test-report-action@v1.2.0 - if: ${{ always() }} \ No newline at end of file + if: ${{ always() }} diff --git a/.github/workflows/draft_new_release-v2.yml b/.github/workflows/draft_new_release-v2.yml index f36c52675..edd0be8a7 100644 --- a/.github/workflows/draft_new_release-v2.yml +++ b/.github/workflows/draft_new_release-v2.yml @@ -92,4 +92,4 @@ jobs: github_token: ${{ secrets.PAT }} pr_title: 'chore(release): pulling ${{ steps.create-release.outputs.branch_name }} into master-v2' pr_body: ':crown: *An automated PR*' - pr_reviewer: 'itsdebs' \ No newline at end of file + pr_reviewer: '@rudderlabs/sdk-android' \ No newline at end of file diff --git a/.github/workflows/publish-new-github-release-v2.yml b/.github/workflows/publish-new-github-release-v2.yml index 88f05a193..77e51fce2 100644 --- a/.github/workflows/publish-new-github-release-v2.yml +++ b/.github/workflows/publish-new-github-release-v2.yml @@ -77,7 +77,7 @@ jobs: github_token: ${{ secrets.PAT }} pr_title: 'chore(release): pulling master-v2 into develop-v2 post release v${{ steps.extract-version-v2.outputs.release_version }}' pr_body: ':crown: *An automated PR*' - pr_reviewer: 'itsdebs' + pr_reviewer: '@rudderlabs/sdk-android' - name: Delete hotfix release branch v2 uses: koj-co/delete-merged-action@master diff --git a/.gitignore b/.gitignore index fa8a72116..2f2129949 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ local.properties node_modules /.idea/ */build + +# Local Rudderstack file properties +rudderstack.properties diff --git a/CODEOWNERS b/CODEOWNERS index 55364db51..dfe2e63fd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @pallabmaiti @itsdebs \ No newline at end of file +* @rudderlabs/sdk-android \ No newline at end of file diff --git a/app/.gitignore b/android/.gitignore similarity index 100% rename from app/.gitignore rename to android/.gitignore diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 000000000..c0ce2f3b6 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,88 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + + namespace = "com.rudderstack.android" + + compileSdk = RudderstackBuildConfig.Android.COMPILE_SDK + + buildFeatures { + buildFeatures { + buildConfig = true + } + } + defaultConfig { + minSdk = RudderstackBuildConfig.Android.MIN_SDK + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + buildConfigField( + "String", + "LIBRARY_VERSION_NAME", + RudderstackBuildConfig.Version.VERSION_NAME + ) + } + + buildTypes { + named("release") { + isMinifyEnabled = false + setProguardFiles( + listOf( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + ) + } + } + compileOptions { + sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + } + kotlinOptions { + jvmTarget = RudderstackBuildConfig.Build.JVM_TARGET + javaParameters = true + } + testOptions { + unitTests { + this.isIncludeAndroidResources = true + } + } +} + +dependencies { + implementation(libs.android.core.ktx) + api(project(":core")) + api(project(":repository")) + + compileOnly(project(":jacksonrudderadapter")) + compileOnly(project(":gsonrudderadapter")) + compileOnly(project(":moshirudderadapter")) + compileOnly(project(":rudderjsonadapter")) + compileOnly(libs.work) + compileOnly(libs.work.multiprocess) + + testImplementation(project(":jacksonrudderadapter")) + testImplementation(project(":gsonrudderadapter")) + testImplementation(project(":moshirudderadapter")) + testImplementation(project(":rudderjsonadapter")) + testImplementation(project(":libs:test-common")) + testImplementation(libs.android.x.test) + testImplementation(libs.android.x.testrules) + testImplementation(libs.android.x.test.ext.junitktx) + testImplementation(libs.awaitility) + testImplementation(libs.hamcrest) + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockk) + testImplementation(libs.mockk.agent) + testImplementation(libs.robolectric) + testImplementation(libs.work.test) + + androidTestImplementation(libs.android.x.test.ext.junitktx) +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-aar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/android/gradle.properties b/android/gradle.properties index e6e099b42..776df7697 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,5 @@ POM_ARTIFACT_ID=android +GROUP=com.rudderstack.android.sdk POM_PACKAGING=aar android.useAndroidX=true android.enableJetifier=true diff --git a/app/proguard-rules.pro b/android/proguard-rules.pro similarity index 100% rename from app/proguard-rules.pro rename to android/proguard-rules.pro diff --git a/android/project.json b/android/project.json index 6dd90a1c0..01ec24022 100644 --- a/android/project.json +++ b/android/project.json @@ -6,6 +6,16 @@ "targets": { "build": { "executor": "@jnxplus/nx-gradle:build", + "dependsOn": [ + { + "projects": [ + "core", + "repository", + "rudderreporter" + ], + "target": "build" + } + ], "outputs": [ "{projectRoot}/android/build" ] diff --git a/app/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml similarity index 66% rename from app/src/main/AndroidManifest.xml rename to android/src/main/AndroidManifest.xml index d55dee4fb..885f61da3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file diff --git a/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt b/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt new file mode 100644 index 000000000..83eceb781 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/AnalyticsRegistry.kt @@ -0,0 +1,59 @@ +package com.rudderstack.android + +import androidx.annotation.VisibleForTesting +import com.rudderstack.core.utilities.FutureUse +import com.rudderstack.core.Analytics +import java.util.concurrent.ConcurrentHashMap + +/** + * AnalyticsRegistry is a singleton object responsible for managing and providing instances of the Analytics class. + * It maintains a mapping between write keys and Analytics instances using concurrent hash maps. + * + * The class provides methods for registering new Analytics instances with unique write keys, + * as well as retrieving instances based on write keys. + * + * Usage: + * - To register a new Analytics instance, use the [register] method, providing a write key and the Analytics instance. + * - To retrieve an Analytics instance, use the [getInstance] method, passing the write key. + * + * Note: The class is marked as internal, indicating that it is intended for use within the same module and should not be accessed + * from outside the module. + */ + +@FutureUse("This class will be utilized when multiple instances are implemented.") +internal object AnalyticsRegistry { + + private val writeKeyToInstance: ConcurrentHashMap = ConcurrentHashMap() + + /** + * Registers a new Analytics instance with the provided write key and Analytics instance. + * If an instance with the same write key already exists, it will not be overwritten. + * + * @param writeKey The unique identifier associated with the Analytics instance. + * @param analytics The Analytics instance to be registered. + */ + @JvmStatic + fun register(writeKey: String, analytics: Analytics) { + writeKeyToInstance.putIfAbsent(writeKey, analytics) + } + + /** + * Retrieves an Analytics instance based on the provided write key. + * + * @param writeKey The write key associated with the desired Analytics instance. + * @return The Analytics instance if found, otherwise null. + */ + @JvmStatic + fun getInstance(writeKey: String): Analytics? { + return writeKeyToInstance[writeKey] + } + + /** + * Clears the mapping of write keys to Analytics instances. + * Note: This method is intended for use in testing scenarios only. + */ + @VisibleForTesting + fun clear() { + writeKeyToInstance.clear() + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rudderstack/android/AndroidUtils.kt b/android/src/main/java/com/rudderstack/android/AndroidUtils.kt new file mode 100644 index 000000000..a97631a04 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/AndroidUtils.kt @@ -0,0 +1,141 @@ +package com.rudderstack.android + +import android.app.Application +import android.app.UiModeManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.os.Build +import android.provider.Settings +import android.text.TextUtils +import android.util.Base64 +import android.util.DisplayMetrics +import android.view.WindowManager +import com.rudderstack.android.models.RudderApp +import com.rudderstack.android.models.RudderDeviceInfo +import com.rudderstack.android.models.RudderScreenInfo +import com.rudderstack.core.Base64Generator +import java.io.UnsupportedEncodingException +import java.util.* + +internal object AndroidUtils { + fun generateAnonymousId(isCollectDeviceId: Boolean, application: Application): String { + return if (!isCollectDeviceId) UUID.randomUUID().toString() else getDeviceId(application) + } + + internal fun getDeviceId(application: Application): String = run { + + val androidId = + Settings.System.getString(application.contentResolver, Settings.Secure.ANDROID_ID) + if (!TextUtils.isEmpty(androidId) && "9774d56d682e549c" != androidId && "unknown" != androidId && "000000000000000" != androidId) { + return androidId + } + androidId ?: UUID.randomUUID().toString() + } + + fun getWriteKeyFromStrings(context: Context): String? { + val id = context.resources.getIdentifier( + context.packageName, "string", "rudder_write_key" + ) + return if (id != 0) { + context.resources.getString(id) + } else { + null + } + } + + internal fun getUTF8Length(message: String): Int { + return try { + message.toByteArray(charset("UTF-8")).size + } catch (ex: UnsupportedEncodingException) { +// RudderLogger.logError(ex); + -1 + } + } + + internal fun getUTF8Length(message: StringBuilder): Int { + return getUTF8Length(message.toString()) + } + + internal fun isOnClassPath(className: String): Boolean { + return try { + Class.forName(className) + true + } catch (e: ClassNotFoundException) { + false + } + } + + /** + * Returns whether the app is running on a TV device. + * + * @param context Any context. + * @return Whether the app is running on a TV device. + */ + internal fun Context.isTv(): Boolean { + val uiModeManager = + applicationContext.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager + return (uiModeManager != null && uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) + } + + internal fun defaultBase64Generator() = Base64Generator { + Base64.encodeToString( + String.format(Locale.US, "%s:", it).toByteArray(Charsets.UTF_8), Base64.NO_WRAP + ) + } + + private fun Application.generateDeviceInfo( + advertisingId: String?, + deviceToken: String, + collectDeviceId: Boolean + ): RudderDeviceInfo { + val deviceId = if (collectDeviceId) getDeviceId(this) else null + return RudderDeviceInfo( + deviceId = deviceId, + manufacturer = Build.MANUFACTURER, + model = Build.MODEL, + name = Build.DEVICE, + token = deviceToken, + isAdTrackingEnabled = advertisingId != null + ).also { + if (advertisingId != null) it.advertisingId = advertisingId + } + } + + + private val Application.rudderApp: RudderApp? + get() = try { + val packageName: String = this.packageName + val packageManager: PackageManager = packageManager + val packageInfo = packageManager.getPackageInfo(packageName, 0) + val build = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode.toString() else packageInfo.versionCode.toString() + val name = packageInfo.applicationInfo.loadLabel(packageManager).toString() + val version = packageInfo.versionName + RudderApp( + name = name, version = version, build = build, nameSpace = packageName + ) + } catch (ex: PackageManager.NameNotFoundException) { +// ReportManager.reportError(ex) +// RudderLogger.logError(ex.cause) + null + } + private val Application.screen: RudderScreenInfo? + get() = run { + val manager = getSystemService(Context.WINDOW_SERVICE) as? WindowManager + return@run if (manager == null) { + null + } else { + val display = manager.defaultDisplay + val displayMetrics = DisplayMetrics() + display.getMetrics(displayMetrics) + RudderScreenInfo( + displayMetrics.densityDpi, + displayMetrics.widthPixels, + displayMetrics.heightPixels + ) + } + + } + +} diff --git a/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt b/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt new file mode 100644 index 000000000..9cc45848a --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/ConfigurationAndroid.kt @@ -0,0 +1,95 @@ +/* + * Creator: Debanjan Chatterjee on 28/11/23, 5:37 pm Last modified: 28/11/23, 10:00 am + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android + +import android.app.Application +import androidx.annotation.RestrictTo +import com.rudderstack.android.internal.AndroidLogger +import com.rudderstack.core.Base64Generator +import com.rudderstack.core.Configuration +import com.rudderstack.core.Logger +import com.rudderstack.core.RetryStrategy +import com.rudderstack.core.RudderOption +import com.rudderstack.rudderjsonadapter.JsonAdapter +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * Data class representing the Android-specific configuration for the RudderStack analytics SDK. + * + */ +data class ConfigurationAndroid @JvmOverloads constructor( + val application: Application, + val anonymousId: String? = null, + val trackLifecycleEvents: Boolean = TRACK_LIFECYCLE_EVENTS, + val recordScreenViews: Boolean = RECORD_SCREEN_VIEWS, + val isPeriodicFlushEnabled: Boolean = IS_PERIODIC_FLUSH_ENABLED, + val autoCollectAdvertId: Boolean = AUTO_COLLECT_ADVERT_ID, + val multiProcessEnabled: Boolean = MULTI_PROCESS_ENABLED, + val defaultProcessName: String? = DEFAULT_PROCESS_NAME, + val advertisingId: String? = null, + val deviceToken: String? = null, + val logLevel: Logger.LogLevel = Logger.DEFAULT_LOG_LEVEL, + val collectDeviceId: Boolean = COLLECT_DEVICE_ID, + val advertisingIdFetchExecutor: ExecutorService = Executors.newCachedThreadPool(), + val trackAutoSession: Boolean = AUTO_SESSION_TRACKING, + val sessionTimeoutMillis: Long = SESSION_TIMEOUT, + override val jsonAdapter: JsonAdapter, + override val options: RudderOption = RudderOption(), + override val flushQueueSize: Int = DEFAULT_FLUSH_QUEUE_SIZE, + override val maxFlushInterval: Long = DEFAULT_MAX_FLUSH_INTERVAL, + override val shouldVerifySdk: Boolean = SHOULD_VERIFY_SDK, + override val gzipEnabled: Boolean = GZIP_ENABLED, + override val sdkVerifyRetryStrategy: RetryStrategy = RetryStrategy.exponential(), + override val dataPlaneUrl: String = DEFAULT_ANDROID_DATAPLANE_URL, + override val controlPlaneUrl: String = DEFAULT_ANDROID_CONTROLPLANE_URL, + override val analyticsExecutor: ExecutorService = Executors.newSingleThreadExecutor(), + override val networkExecutor: ExecutorService = Executors.newCachedThreadPool(), + override val base64Generator: Base64Generator = AndroidUtils.defaultBase64Generator(), +) : Configuration ( + jsonAdapter = jsonAdapter, + options = options, + flushQueueSize = flushQueueSize, + maxFlushInterval = maxFlushInterval, + shouldVerifySdk = shouldVerifySdk, + gzipEnabled = gzipEnabled, + sdkVerifyRetryStrategy = sdkVerifyRetryStrategy, + dataPlaneUrl = dataPlaneUrl, + controlPlaneUrl = controlPlaneUrl, + logger = AndroidLogger(logLevel), + analyticsExecutor = analyticsExecutor, + networkExecutor = networkExecutor, + base64Generator = base64Generator, +) { + companion object { + const val COLLECT_DEVICE_ID: Boolean = true + const val DEFAULT_ANDROID_DATAPLANE_URL = "https://hosted.rudderlabs.com" + const val DEFAULT_ANDROID_CONTROLPLANE_URL = "https://api.rudderlabs.com" + const val GZIP_ENABLED: Boolean = true + const val SHOULD_VERIFY_SDK: Boolean = true + const val TRACK_LIFECYCLE_EVENTS = true + const val RECORD_SCREEN_VIEWS = false + const val IS_PERIODIC_FLUSH_ENABLED = false + const val AUTO_COLLECT_ADVERT_ID = false + const val MULTI_PROCESS_ENABLED = false + @JvmField + var DEFAULT_PROCESS_NAME: String? = null + const val USE_CONTENT_PROVIDER = false + const val DEFAULT_FLUSH_QUEUE_SIZE = 30 + const val DEFAULT_MAX_FLUSH_INTERVAL = 10 * 1000L + const val SESSION_TIMEOUT: Long = 300000 + const val AUTO_SESSION_TRACKING = true + } +} diff --git a/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt b/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt new file mode 100644 index 000000000..3e4913c01 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/LifecycleListenerPlugin.kt @@ -0,0 +1,82 @@ +/* + * Creator: Debanjan Chatterjee on 21/11/23, 12:13 pm Last modified: 21/11/23, 12:13 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android + +import android.app.Activity +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.Plugin + +/** + * Any [InfrastructurePlugin] or [Plugin] that wants to listen to lifecycle events should implement + * this interface. + * + * onAppForegrounded() and onAppBackgrounded() are called when the app goes to foreground and background respectively. + * These methods are called only if [ConfigurationAndroid.trackLifecycleEvents] is set to true. + * + * onActivityStarted(), onActivityStopped() and screenChange() are called when an activity is + * started and stopped respectively. + * These methods are called only if both [ConfigurationAndroid.trackLifecycleEvents] and + * [ConfigurationAndroid.recordScreenViews] are set to true. + * + * Getting callbacks is subject to availability of Infrastructure plugins that are designed + * to listen to lifecycle events. + * + */ +interface LifecycleListenerPlugin { + /** + * Called when at least one activity is started + * + */ + fun onAppForegrounded(){} + /** + * Called when all activities are stopped + * + */ + fun onAppBackgrounded(){} + + /** + * Called when an activity is started + * + * @param activityName The name of the activity that is started + */ + fun onActivityStarted(activityName: String){} + /** + * Called when the current activity is changed + * Will be null if the app is backgrounded + * If using this method, make sure to make it thread safe. + * Storing the activity as a strong reference might lead to memory leaks. + * If you're only interested in the name of the activity, use [onActivityStarted] instead. + * + * @param activity The activity that is started + */ + fun setCurrentActivity(activity: Activity?){} + + /** + * Called when an activity is stopped + * + * @param activityName The name of the activity that is stopped + */ + fun onActivityStopped(activityName: String){} + + /** + * Called when the screen changes. + * This is mostly associated to fragment or compose navigation. + * + * + * @param name Name of the compose route or fragment label + * @param arguments The arguments passed to the to the composable or fragment in question. + */ + fun onScreenChange(name: String, arguments: Map?){} +} \ No newline at end of file diff --git a/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt b/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt new file mode 100644 index 000000000..39fc441de --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/RudderAnalytics.kt @@ -0,0 +1,63 @@ +package com.rudderstack.android + +import com.rudderstack.android.storage.AndroidStorageImpl +import com.rudderstack.android.utilities.onShutdown +import com.rudderstack.android.utilities.startup +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.Storage + +/** + * Singleton class for RudderAnalytics to manage the analytics instance. + * + * This class ensures that only one instance of the Analytics object is created. + */ +class RudderAnalytics private constructor() { + + companion object { + + @Volatile + private var instance: Analytics? = null + + /** + * Returns the singleton instance of [Analytics], creating it if necessary. + * + * @param writeKey The write key for authentication. + * @param configuration The configuration settings for Android. + * @param storage The storage implementation for storing data. Defaults to [AndroidStorageImpl]. + * @param dataUploadService The service responsible for uploading data. Defaults to null. + * @param configDownloadService The service responsible for downloading configuration. Defaults to null. + * @param initializationListener A listener for initialization events. Defaults to null. + * @return The singleton instance of [Analytics]. + */ + @JvmStatic + @JvmOverloads + fun getInstance( + writeKey: String, + configuration: ConfigurationAndroid, + storage: Storage = AndroidStorageImpl( + configuration.application, + writeKey = writeKey, + useContentProvider = ConfigurationAndroid.USE_CONTENT_PROVIDER + ), + dataUploadService: DataUploadService? = null, + configDownloadService: ConfigDownloadService? = null, + initializationListener: ((success: Boolean, message: String?) -> Unit)? = null, + ) = instance ?: synchronized(this) { + instance ?: Analytics( + writeKey = writeKey, + configuration = configuration, + dataUploadService = dataUploadService, + configDownloadService = configDownloadService, + storage = storage, + initializationListener = initializationListener, + shutdownHook = { onShutdown() } + ).apply { + startup() + }.also { + instance = it + } + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java b/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java new file mode 100644 index 000000000..38e374785 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/compat/ConfigurationAndroidBuilder.java @@ -0,0 +1,118 @@ +package com.rudderstack.android.compat; + +import android.app.Application; + +import com.rudderstack.android.AndroidUtils; +import com.rudderstack.android.ConfigurationAndroid; +import com.rudderstack.core.compat.ConfigurationBuilder; +import com.rudderstack.rudderjsonadapter.JsonAdapter; +import com.rudderstack.core.Logger; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +//Java compatible Builder for [ConfigurationAndroid] +public class ConfigurationAndroidBuilder extends ConfigurationBuilder { + private final Application application ; + private String anonymousId; + private Boolean trackLifecycleEvents = ConfigurationAndroid.TRACK_LIFECYCLE_EVENTS; + private Boolean recordScreenViews = ConfigurationAndroid.RECORD_SCREEN_VIEWS; + private Boolean isPeriodicFlushEnabled = ConfigurationAndroid.IS_PERIODIC_FLUSH_ENABLED; + private Boolean autoCollectAdvertId = ConfigurationAndroid.AUTO_COLLECT_ADVERT_ID; + private Boolean multiProcessEnabled = ConfigurationAndroid.MULTI_PROCESS_ENABLED; + private String defaultProcessName= ConfigurationAndroid.DEFAULT_PROCESS_NAME; + private String advertisingId = null; + private String deviceToken = null; + private boolean collectDeviceId = ConfigurationAndroid.COLLECT_DEVICE_ID; + private ExecutorService advertisingIdFetchExecutor = Executors.newCachedThreadPool(); + private boolean trackAutoSession = ConfigurationAndroid.AUTO_SESSION_TRACKING; + private long sessionTimeoutMillis = ConfigurationAndroid.SESSION_TIMEOUT; + private Logger.LogLevel logLevel = Logger.DEFAULT_LOG_LEVEL; + + public ConfigurationAndroidBuilder(Application application, JsonAdapter jsonAdapter) { + super(jsonAdapter); + this.application = application; + anonymousId = AndroidUtils.INSTANCE.generateAnonymousId(collectDeviceId, application); + } + public ConfigurationBuilder withAnonymousId(String anonymousId) { + this.anonymousId = anonymousId; + return this; + } + public ConfigurationBuilder withTrackLifecycleEvents(Boolean trackLifecycleEvents) { + this.trackLifecycleEvents = trackLifecycleEvents; + return this; + } + public ConfigurationBuilder withRecordScreenViews(Boolean recordScreenViews) { + this.recordScreenViews = recordScreenViews; + return this; + } + public ConfigurationBuilder withIsPeriodicFlushEnabled(Boolean isPeriodicFlushEnabled) { + this.isPeriodicFlushEnabled = isPeriodicFlushEnabled; + return this; + } + public ConfigurationBuilder withAutoCollectAdvertId(Boolean autoCollectAdvertId) { + this.autoCollectAdvertId = autoCollectAdvertId; + return this; + } + public ConfigurationBuilder withMultiProcessEnabled(Boolean multiProcessEnabled) { + this.multiProcessEnabled = multiProcessEnabled; + return this; + } + public ConfigurationBuilder withDefaultProcessName(String defaultProcessName) { + this.defaultProcessName = defaultProcessName; + return this; + } + public ConfigurationBuilder withAdvertisingId(String advertisingId) { + this.advertisingId = advertisingId; + return this; + } + public ConfigurationBuilder withDeviceToken(String deviceToken) { + this.deviceToken = deviceToken; + return this; + } + public ConfigurationBuilder withAdvertisingIdFetchExecutor(ExecutorService advertisingIdFetchExecutor) { + this.advertisingIdFetchExecutor = advertisingIdFetchExecutor; + return this; + } + public ConfigurationBuilder withTrackAutoSession(boolean trackAutoSession) { + this.trackAutoSession = trackAutoSession; + return this; + } + public ConfigurationBuilder withSessionTimeoutMillis(long sessionTimeoutMillis) { + this.sessionTimeoutMillis = sessionTimeoutMillis; + return this; + } + + public ConfigurationBuilder withLogLevel(Logger.LogLevel logLevel) { + this.logLevel = logLevel; + return this; + } + + public ConfigurationBuilder withCollectDeviceId(boolean collectDeviceId) { + this.collectDeviceId = collectDeviceId; + return this; + } + + @Override + public ConfigurationAndroid build() { + return new ConfigurationAndroid( + application, + anonymousId, + trackLifecycleEvents, + recordScreenViews, + isPeriodicFlushEnabled, + autoCollectAdvertId, + multiProcessEnabled, + defaultProcessName, + advertisingId, + deviceToken, + logLevel, + collectDeviceId, + advertisingIdFetchExecutor, + trackAutoSession, + sessionTimeoutMillis, + jsonAdapter + ); + } + +} diff --git a/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java b/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java new file mode 100644 index 000000000..8afc4b279 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/compat/RudderAnalyticsBuilderCompat.java @@ -0,0 +1,75 @@ +package com.rudderstack.android.compat; + + +import androidx.annotation.NonNull; + +import com.rudderstack.android.ConfigurationAndroid; +import com.rudderstack.android.RudderAnalytics; +import com.rudderstack.android.storage.AndroidStorage; +import com.rudderstack.android.storage.AndroidStorageImpl; +import com.rudderstack.core.Analytics; +import com.rudderstack.core.ConfigDownloadService; +import com.rudderstack.core.DataUploadService; + +import java.util.concurrent.Executors; + +import kotlin.Unit; + +/** + * To be used by java projects + */ +public final class RudderAnalyticsBuilderCompat { + + private @NonNull String writeKey; + private @NonNull ConfigurationAndroid configuration; + private DataUploadService dataUploadService = null; + private ConfigDownloadService configDownloadService = null; + private InitializationListener initializationListener = null; + private AndroidStorage storage = new AndroidStorageImpl(configuration.getApplication(), + ConfigurationAndroid.USE_CONTENT_PROVIDER, + writeKey, + Executors.newSingleThreadExecutor()); + + public RudderAnalyticsBuilderCompat(@NonNull String writeKey, @NonNull ConfigurationAndroid configuration) { + this.writeKey = writeKey; + this.configuration = configuration; + } + public RudderAnalyticsBuilderCompat withDataUploadService(DataUploadService dataUploadService) { + this.dataUploadService = dataUploadService; + return this; + } + public RudderAnalyticsBuilderCompat withConfigDownloadService(ConfigDownloadService configDownloadService) { + this.configDownloadService = configDownloadService; + return this; + } + public RudderAnalyticsBuilderCompat withInitializationListener(InitializationListener initializationListener) { + this.initializationListener = initializationListener; + return this; + } + public Analytics build() { + + return RudderAnalytics.getInstance( + writeKey, + configuration, + storage, + dataUploadService, + configDownloadService, + (success, message) -> { + if (initializationListener != null) { + initializationListener.onInitialized(success, message); + } + return Unit.INSTANCE; + } + ); + } + + public interface InitializationListener { + void onInitialized(boolean success, String message); + } + + public RudderAnalyticsBuilderCompat withStorage(AndroidStorage storage) { + this.storage = storage; + return this; + } + +} diff --git a/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt b/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt new file mode 100644 index 000000000..0df5f0e34 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/AndroidLogger.kt @@ -0,0 +1,43 @@ +package com.rudderstack.android.internal + +import android.util.Log +import com.rudderstack.core.Logger + +/** + * Logger implementation specifically for android. + * + */ +class AndroidLogger( + initialLogLevel: Logger.LogLevel = Logger.DEFAULT_LOG_LEVEL +) : Logger { + private var logLevel: Logger.LogLevel = initialLogLevel + @Synchronized set + @Synchronized get + + override fun activate(level: Logger.LogLevel) { + logLevel = level + } + + override fun info(tag: String, log: String) { + if (Logger.LogLevel.INFO >= logLevel) + Log.i(tag, log) + } + + override fun debug(tag: String, log: String) { + if (Logger.LogLevel.DEBUG >= logLevel) + Log.d(tag, log) + } + + override fun warn(tag: String, log: String) { + if (Logger.LogLevel.WARN >= logLevel) + Log.w(tag, log) + } + + override fun error(tag: String, log: String, throwable: Throwable?) { + if (Logger.LogLevel.ERROR >= logLevel) + Log.e(tag, log, throwable) + } + + override val level: Logger.LogLevel + get() = logLevel +} diff --git a/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt b/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt new file mode 100644 index 000000000..7c4430cd4 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/DmtUtils.kt @@ -0,0 +1,29 @@ +package com.rudderstack.android.internal + + +/** + * [STATUS_NEW] should be the initial status. + * Any further operations should be masked with appropriate status + * for eg: + * for setting cloud mode done the way should be + ``` + var status = STATUS_NEW + status = status maskWith STATUS_CLOUD_MODE_DONE + ``` + */ +internal const val STATUS_NEW = 0 +internal const val STATUS_CLOUD_MODE_DONE = 2 +internal const val STATUS_DEVICE_MODE_DONE = 1 + +infix fun Int.maskWith(status: Int) : Int{ + return this or (1 shl status) +} +infix fun Int.unmaskWith(status: Int) : Int{ + return this and (1 shl status).inv() +} +fun Int.isDeviceModeDone() : Boolean{ + return this and (1 shl STATUS_DEVICE_MODE_DONE) != 0 +} +fun Int.isCloudModeDone() : Boolean{ + return this and (1 shl STATUS_CLOUD_MODE_DONE) != 0 +} diff --git a/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt b/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt new file mode 100644 index 000000000..36b059874 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/RudderPreferenceManager.kt @@ -0,0 +1,237 @@ +/* + * Creator: Debanjan Chatterjee on 04/07/22, 4:45 PM Last modified: 04/07/22, 4:45 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.internal + +import android.app.Application +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences +import android.os.Build +import java.io.File + +// keys +private const val RUDDER_PREFS = "rl_prefs" +private const val RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY = "rl_server_last_updated" +private const val RUDDER_TRAITS_KEY = "rl_traits" +private const val RUDDER_APPLICATION_INFO_KEY = "rl_application_info_key" +private const val RUDDER_TRACK_AUTO_SESSION_KEY = "rl_track_auto_session_key" +private const val RUDDER_EXTERNAL_ID_KEY = "rl_external_id" +private const val RUDDER_OPT_STATUS_KEY = "rl_opt_status" +private const val RUDDER_OPT_IN_TIME_KEY = "rl_opt_in_time" +private const val RUDDER_OPT_OUT_TIME_KEY = "rl_opt_out_time" +private const val RUDDER_ANONYMOUS_ID_KEY = "rl_anonymous_id_key" +private const val RUDDER_USER_ID_KEY = "rl_user_id_key" +private const val RUDDER_PERIODIC_WORK_REQUEST_ID_KEY = "rl_periodic_work_request_key" +private const val RUDDER_SESSION_ID_KEY = "rl_session_id_key" +private const val RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY = + "rl_last_event_timestamp_key" +private const val RUDDER_ADVERTISING_ID_KEY = "rl_advertising_id_key" + +private const val RUDDER_APPLICATION_VERSION_KEY = "rl_application_version_key" +private const val RUDDER_APPLICATION_BUILD_KEY = "rl_application_build_key" +internal class RudderPreferenceManager(private val application: Application, + private val writeKey: String) { + + private val String.key: String + get() = "$this-$writeKey" + + private var preferences: SharedPreferences = + application.getSharedPreferences(RUDDER_PREFS.key, Context.MODE_PRIVATE) + private var preferencesV1: SharedPreferences = + application.getSharedPreferences(RUDDER_PREFS, Context.MODE_PRIVATE) + val lastUpdatedTime: Long + get() = preferences.getLong(RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY.key, + -1) + + fun updateLastUpdatedTime() { + preferences.edit().putLong(RUDDER_SERVER_CONFIG_LAST_UPDATE_KEY.key, System + .currentTimeMillis()) + .apply() + } + + val traits: String? + get() = preferences.getString(RUDDER_TRAITS_KEY.key, null) + + fun saveTraits(traitsJson: String?) { + preferences.edit().putString(RUDDER_TRAITS_KEY.key, traitsJson).apply() + } + val advertisingId: String? + get() = preferences.getString(RUDDER_ADVERTISING_ID_KEY.key, null) + + fun saveAdvertisingId(advertisingId: String?) { + preferences.edit().putString(RUDDER_ADVERTISING_ID_KEY.key, advertisingId).apply() + } + + val buildVersionCode: Int + get() = preferences.getInt(RUDDER_APPLICATION_INFO_KEY.key, -1) + + fun saveBuildVersionCode(versionCode: Int) { + preferences.edit().putInt(RUDDER_APPLICATION_INFO_KEY.key, versionCode).apply() + } + + val externalIds: String? + get() = preferences.getString(RUDDER_EXTERNAL_ID_KEY.key, null) + + fun saveExternalIds(externalIdsJson: String?) { + preferences.edit().putString(RUDDER_EXTERNAL_ID_KEY.key, externalIdsJson).apply() + } + + fun clearExternalIds() { + preferences.edit().remove(RUDDER_EXTERNAL_ID_KEY.key).apply() + } + + fun saveAnonymousId(anonymousId: String?) { + preferences.edit().putString(RUDDER_ANONYMOUS_ID_KEY.key, anonymousId).apply() + } + fun saveSessionId(sessionId: Long?) { + preferences.edit().putLong(RUDDER_SESSION_ID_KEY.key, sessionId?:-1L).apply() + } + fun clearSessionId() { + preferences.edit().remove(RUDDER_SESSION_ID_KEY.key).apply() + } + val sessionId: Long + get() = preferences.getLong(RUDDER_SESSION_ID_KEY.key, -1L) + fun saveLastActiveTimestamp(lastActiveTimestamp: Long?) { + preferences.edit().putLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key, lastActiveTimestamp + ?: -1L).apply() + } + fun clearLastActiveTimestamp() { + preferences.edit().remove(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key).apply() + } + val lastActiveTimestamp: Long + get() = preferences.getLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY.key, -1L) + + val anonymousId: String? + get() = preferences.getString(RUDDER_ANONYMOUS_ID_KEY.key, null) + fun saveUserId(userId: String?) { + preferences.edit().putString(RUDDER_USER_ID_KEY.key, userId).apply() + } + fun saveTrackAutoSession(trackAutoSession: Boolean) { + preferences.edit().putBoolean(RUDDER_TRACK_AUTO_SESSION_KEY.key, trackAutoSession).apply() + } + + internal val trackAutoSession: Boolean + get() = preferences.getBoolean(RUDDER_TRACK_AUTO_SESSION_KEY, false) + + val userId: String? + get() = preferences.getString(RUDDER_USER_ID_KEY.key, null) + + fun updateOptInTime() { + preferences.edit().putLong(RUDDER_OPT_IN_TIME_KEY.key, System.currentTimeMillis()).apply() + } + + fun updateOptOutTime() { + preferences.edit().putLong(RUDDER_OPT_OUT_TIME_KEY.key, System.currentTimeMillis()).apply() + } + + val optInTime: Long + get() = preferences.getLong(RUDDER_OPT_IN_TIME_KEY.key, -1) + val optOutTime: Long + get() = preferences.getLong(RUDDER_OPT_OUT_TIME_KEY.key, -1) + + fun savePeriodicWorkRequestId(periodicWorkRequestId: String?) { + preferences.edit().putString(RUDDER_PERIODIC_WORK_REQUEST_ID_KEY.key, periodicWorkRequestId) + .apply() + } + + fun resetV1AnonymousId() { + preferencesV1.edit().remove(RUDDER_ANONYMOUS_ID_KEY).apply() + } + fun resetV1Traits() { + preferencesV1.edit().remove(RUDDER_TRAITS_KEY).apply() + } + fun resetV1ExternalIds() { + preferencesV1.edit().remove(RUDDER_EXTERNAL_ID_KEY).apply() + } + fun resetV1OptOut() { + preferencesV1.edit().remove(RUDDER_OPT_STATUS_KEY).apply() + } + + val periodicWorkRequestId: String? + get() = preferences.getString(RUDDER_PERIODIC_WORK_REQUEST_ID_KEY.key, null) + val v1AnonymousId + get() = preferencesV1.getString(RUDDER_ANONYMOUS_ID_KEY, null) + + internal val v1ExternalIdsJson: String? + get() = preferencesV1.getString(RUDDER_EXTERNAL_ID_KEY, null) + + internal val v1Traits + get() = preferencesV1.getString(RUDDER_TRAITS_KEY, null) + + internal val v1optOutStatus: Boolean + get() = preferencesV1.getBoolean(RUDDER_OPT_STATUS_KEY, false) + + internal val v1LastActiveTimestamp: Long? + get() = preferencesV1.getLong(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY, -1L).takeIf { + it > 0 + } + + internal val v1SessionId : Long + get() = preferencesV1.getLong(RUDDER_SESSION_ID_KEY, -1) + internal val v1Build: Int + get() = preferencesV1.getInt(RUDDER_APPLICATION_BUILD_KEY, -1) + internal val v1VersionName: String? + get() = preferencesV1.getString(RUDDER_APPLICATION_VERSION_KEY, null) + fun saveOptStatus(optStatus: Boolean) { + preferences.edit().putBoolean(RUDDER_OPT_STATUS_KEY.key, optStatus).apply() + } + + val optStatus: Boolean + get() = preferences.getBoolean(RUDDER_OPT_STATUS_KEY.key, false) + + fun saveVersionName(versionName: String) { + preferences.edit().putString(RUDDER_APPLICATION_VERSION_KEY.key, versionName).apply() + } + + val versionName: String? + get() = preferences.getString(RUDDER_APPLICATION_VERSION_KEY.key, null) + + fun saveBuild(build: Int) { + preferences.edit().putInt(RUDDER_APPLICATION_BUILD_KEY.key, build).apply() + } + + val build: Int + get() = preferences.getInt(RUDDER_APPLICATION_BUILD_KEY.key, -1) + + val v1AdvertisingId: String? + get() = preferencesV1.getString(RUDDER_ADVERTISING_ID_KEY, null) + + fun resetV1AdvertisingId() { + preferencesV1.edit().remove(RUDDER_ADVERTISING_ID_KEY).apply() + } + fun resetV1SessionId() { + preferencesV1.edit().remove(RUDDER_SESSION_ID_KEY).apply() + } + fun resetV1LastActiveTimestamp() { + preferencesV1.edit().remove(RUDDER_SESSION_LAST_ACTIVE_TIMESTAMP_KEY).apply() + } + fun resetV1VersionName(){ + preferencesV1.edit().remove(RUDDER_APPLICATION_VERSION_KEY).apply() + } + + fun resetV1Build() { + preferencesV1.edit().remove(RUDDER_APPLICATION_BUILD_KEY).apply() + } + + fun deleteV1PreferencesFile(): Boolean{ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return application.deleteSharedPreferences(RUDDER_PREFS) + } else { + application.getSharedPreferences(RUDDER_PREFS, MODE_PRIVATE).edit().clear().apply() + val dir = File(application.applicationInfo.dataDir, "shared_prefs") + return File(dir, "$RUDDER_PREFS.xml").delete() + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt b/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt new file mode 100644 index 000000000..fa6ad45d8 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/extensions/UserSessionExtensions.kt @@ -0,0 +1,24 @@ +package com.rudderstack.android.internal.extensions + +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.with + +private const val CONTEXT_SESSION_ID_KEY = "sessionId" +private const val CONTEXT_SESSION_START_KEY = "sessionStart" + +internal fun MessageContext.withSessionId(sessionId: String): MessageContext { + return this.with(CONTEXT_SESSION_ID_KEY to sessionId) +} + +internal fun MessageContext.withSessionStart(sessionStart: Boolean): MessageContext { + return this.with(CONTEXT_SESSION_START_KEY to sessionStart) +} + +internal fun MessageContext.removeSessionContext(): MessageContext { + return this.minus(listOf(CONTEXT_SESSION_ID_KEY, CONTEXT_SESSION_START_KEY)) +} + +internal val MessageContext.sessionId: String? + get() = this[CONTEXT_SESSION_ID_KEY] as? String +internal val MessageContext.sessionStart: Boolean? + get() = this[CONTEXT_SESSION_START_KEY] as? Boolean diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt new file mode 100644 index 000000000..d574ed704 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ActivityBroadcasterPlugin.kt @@ -0,0 +1,122 @@ +package com.rudderstack.android.internal.infrastructure + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.rudderstack.android.LifecycleListenerPlugin +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin +import java.util.concurrent.atomic.AtomicInteger + +/** Tracks the Activities in the application and broadcasts the same */ +internal class ActivityBroadcasterPlugin : InfrastructurePlugin { + + override lateinit var analytics: Analytics + private val application: Application? + get() = analytics.currentConfigurationAndroid?.application + + private val activityCount = AtomicInteger() + + override fun setup(analytics: Analytics) { + super.setup(analytics) + application?.registerActivityLifecycleCallbacks(lifecycleCallback) + } + + private val lifecycleCallback by lazy { + object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + incrementActivityCount() + if (activityCount.get() == 1) { + broadCastApplicationStart() + } + broadcastActivityStart(activity) + } + + override fun onActivityResumed(activity: Activity) { + //nothing to implement + } + + override fun onActivityPaused(activity: Activity) { + //nothing to implement + } + + override fun onActivityStopped(activity: Activity) { + if (analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) { + decrementActivityCount() + if (activityCount.get() == 0) { + broadCastApplicationStop() + } + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + //nothing to implement + } + + override fun onActivityDestroyed(activity: Activity) { + //nothing to implement + } + } + } + + private fun broadcastActivityStart(activity: Activity) { + analytics.applyInfrastructureClosure { + if (this is LifecycleListenerPlugin) { + setCurrentActivity(activity) + onActivityStarted(activity.localClassName) + } + } + analytics.applyMessageClosure { + if (this is LifecycleListenerPlugin) { + setCurrentActivity(activity) + onActivityStarted(activity.localClassName) + } + } + } + + private fun decrementActivityCount() { + activityCount.decrementAndGet() + } + + private fun incrementActivityCount() { + activityCount.incrementAndGet() + } + + + private fun broadCastApplicationStart() { + analytics.applyInfrastructureClosure { + if (this is LifecycleListenerPlugin) { + this.onAppForegrounded() + } + } + analytics.applyMessageClosure { + if (this is LifecycleListenerPlugin) { + this.onAppForegrounded() + } + } + } + + private fun broadCastApplicationStop() { + analytics.applyInfrastructureClosure { + if (this is LifecycleListenerPlugin) { + setCurrentActivity(null) + this.onAppBackgrounded() + } + } + analytics.applyMessageClosure { + + if (this is LifecycleListenerPlugin) { + setCurrentActivity(null) + this.onAppBackgrounded() + } + } + } + + override fun shutdown() { + application?.unregisterActivityLifecycleCallbacks(lifecycleCallback) + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt new file mode 100644 index 000000000..5ff69ca29 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AnonymousIdHeaderPlugin.kt @@ -0,0 +1,37 @@ +package com.rudderstack.android.internal.infrastructure + +import com.rudderstack.android.AndroidUtils +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.utilities.applyConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.InfrastructurePlugin + +internal class AnonymousIdHeaderPlugin : InfrastructurePlugin { + + override lateinit var analytics: Analytics + private var dataUploadService: DataUploadService? = null + + override fun setup(analytics: Analytics) { + super.setup(analytics) + dataUploadService = analytics.dataUploadService + } + + override fun updateConfiguration(configuration: Configuration) { + if (configuration !is ConfigurationAndroid) return + val anonId = configuration.anonymousId ?: AndroidUtils.generateAnonymousId( + configuration.collectDeviceId, + configuration.application + ).also { + analytics.applyConfigurationAndroid { + copy(anonymousId = it) + } + } + dataUploadService?.addHeaders(mapOf("Anonymous-Id" to anonId)) + } + + override fun shutdown() { + dataUploadService = null + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt new file mode 100644 index 000000000..0fa6c0948 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPlugin.kt @@ -0,0 +1,129 @@ +package com.rudderstack.android.internal.infrastructure + +import android.content.pm.PackageManager +import android.os.Build +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.utilities.androidStorage +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.AppVersion +import com.rudderstack.core.models.Message + +private const val PREVIOUS_VERSION = "previous_version" +private const val PREVIOUS_BUILD = "previous_build" +private const val VERSION = "version" +private const val BUILD = "build" + +private const val EVENT_NAME_APPLICATION_INSTALLED = "Application Installed" +private const val EVENT_NAME_APPLICATION_UPDATED = "Application Updated" + +private const val DEFAULT_BUILD = -1 +private const val DEFAULT_VERSION_NAME = "" + +/** + * Plugin to check and send the application installed/updated events. + * However an [InfrastructurePlugin] is not suited for generating events on setup + * as [InfrastructurePlugin]s are initialized way before the [Analytics] object is ready to + * transmit events. + * Whereas [Plugin]s are setup after [InfrastructurePlugin]s and are capable to transmit events + * * */ +class AppInstallUpdateTrackerPlugin : Plugin { + + override lateinit var analytics: Analytics + private lateinit var appVersion: AppVersion + override fun intercept(chain: Plugin.Chain): Message { + // no change made to message + return chain.proceed(chain.message()) + } + + override fun setup(analytics: Analytics) { + super.setup(analytics) + this.appVersion = getAppVersion(analytics) + storeVersionNameAndBuild(analytics.androidStorage) + if (this.analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) { + trackApplicationStatus() + } + } + + private fun getAppVersion(analytics: Analytics): AppVersion { + val previousBuild: Int? = analytics.androidStorage.build + val previousVersionName: String? = analytics.androidStorage.versionName + var currentBuild: Int? = null + var currentVersionName: String? = null + + try { + val packageName = analytics.currentConfigurationAndroid?.application?.packageName + val packageManager: PackageManager? = analytics.currentConfigurationAndroid?.application?.packageManager + val packageInfo = packageName?.let { + packageManager?.getPackageInfo(it, 0) + } + + currentVersionName = packageInfo?.versionName + currentBuild = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo?.longVersionCode?.toInt() + } else { + packageInfo?.versionCode + } + } catch (ex: PackageManager.NameNotFoundException) { + analytics.logger.error(log = "Failed to get app version info: ${ex.message}") + } + + return AppVersion( + previousBuild = previousBuild ?: DEFAULT_BUILD, + previousVersionName = previousVersionName ?: DEFAULT_VERSION_NAME, + currentBuild = currentBuild ?: DEFAULT_BUILD, + currentVersionName = currentVersionName ?: DEFAULT_VERSION_NAME, + ) + } + + private fun storeVersionNameAndBuild(analyticsStorage: AndroidStorage) { + analyticsStorage.setVersionName(this.appVersion.currentVersionName) + analyticsStorage.setBuild(this.appVersion.currentBuild) + } + + private fun trackApplicationStatus() { + if (this.isApplicationInstalled()) { + sendApplicationInstalledEvent() + } else if (this.isApplicationUpdated()) { + sendApplicationUpdatedEvent() + } + } + + private fun isApplicationInstalled(): Boolean { + return this.appVersion.previousBuild == -1 + } + + private fun isApplicationUpdated(): Boolean { + return this.appVersion.previousBuild != -1 && this.appVersion.previousBuild != this.appVersion.currentBuild + } + + private fun sendApplicationInstalledEvent() { + this.analytics.logger.debug(log = "Tracking Application Installed event") + val trackProperties = mutableMapOf() + trackProperties[VERSION] = this.appVersion.currentVersionName + trackProperties[BUILD] = this.appVersion.currentBuild + + sendEvent(EVENT_NAME_APPLICATION_INSTALLED, trackProperties) + } + + private fun sendApplicationUpdatedEvent() { + this.analytics.logger.debug(log = "Tracking Application Updated event") + val trackProperties = mutableMapOf() + trackProperties[PREVIOUS_VERSION] = this.appVersion.previousVersionName + trackProperties[PREVIOUS_BUILD] = this.appVersion.previousBuild + trackProperties[VERSION] = this.appVersion.currentVersionName + trackProperties[BUILD] = this.appVersion.currentBuild + sendEvent(EVENT_NAME_APPLICATION_UPDATED, trackProperties) + } + + private fun sendEvent(eventName: String, properties: Map) { + analytics.track { + event(eventName) + trackProperties { + add(properties) + } + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt new file mode 100644 index 000000000..7e75e6d27 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPlugin.kt @@ -0,0 +1,153 @@ +package com.rudderstack.android.internal.infrastructure + +import android.os.SystemClock +import com.rudderstack.android.LifecycleListenerPlugin +import com.rudderstack.android.utilities.androidStorage +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong + +const val EVENT_NAME_APPLICATION_OPENED = "Application Opened" +const val EVENT_NAME_APPLICATION_STOPPED = "Application Backgrounded" +private const val MAX_CONFIG_DOWNLOAD_INTERVAL = 90 * 60 * 1000 // 90 MINUTES + +private const val AUTOMATIC = "automatic" + +/** + * We use a [Plugin] instead of [InfrastructurePlugin] as sending events from Infrastructure plugin might + * not ensure all plugins to be ready + * + * @property currentMillisGenerator + */ +internal class LifecycleObserverPlugin( + val currentMillisGenerator: (() -> Long) = { SystemClock.uptimeMillis() } +) : Plugin, LifecycleListenerPlugin { + + override lateinit var analytics: Analytics + + private var _isFirstLaunch = AtomicBoolean(true) + + private var _lastSuccessfulDownloadTimeInMillis = AtomicLong(-1L) + private var currentActivityName: String? = null + private val listener = ConfigDownloadService.Listener { + if (it) { + _lastSuccessfulDownloadTimeInMillis.set(currentMillisGenerator()) + } + } + + override fun setup(analytics: Analytics) { + super.setup(analytics) + analytics.applyInfrastructureClosure { + if (this is ConfigDownloadService) { + addListener(listener, 1) + } + } + } + + private fun sendLifecycleStart() { + withTrackLifeCycle { + analytics.also { analytics -> + analytics.track { + event(EVENT_NAME_APPLICATION_OPENED) + trackProperties { + _isFirstLaunch.getAndSet(false).also { isFirstLaunch -> + add("from_background" to !isFirstLaunch) + }.takeIf { it }?.let { + add("version" to (analytics.androidStorage.versionName ?: "")) + } + } + } + } + } + } + + private fun sendLifecycleStop() { + withTrackLifeCycle { + analytics.track { + event(EVENT_NAME_APPLICATION_STOPPED) + } + } + } + + override fun intercept(chain: Plugin.Chain): Message { + return chain.proceed(chain.message()) + } + + override fun onShutDown() { + _lastSuccessfulDownloadTimeInMillis.set(0) + analytics.applyInfrastructureClosure { + if (this is ConfigDownloadService) { + removeListener(listener) + } + } + } + + override fun updateConfiguration(configuration: Configuration) { + // no -op + } + + override fun onAppForegrounded() { + sendLifecycleStart() + checkAndDownloadSourceConfig() + } + + private fun checkAndDownloadSourceConfig() { + analytics.takeIf { it.currentConfiguration?.shouldVerifySdk == true }?.apply { + if (currentMillisGenerator() - (_lastSuccessfulDownloadTimeInMillis.get()) >= MAX_CONFIG_DOWNLOAD_INTERVAL) { + analytics.updateSourceConfig() + } + } + } + + override fun onAppBackgrounded() { + sendLifecycleStop() + analytics.flush() + } + + override fun onActivityStarted(activityName: String) { + currentActivityName = activityName + withRecordScreenViews { + analytics.screen { + screenName(activityName) + screenProperties { + add(AUTOMATIC to true) + } + } + } + } + + override fun onActivityStopped(activityName: String) { + //No-Ops + } + + override fun onScreenChange(name: String, arguments: Map?) { + val activityName = currentActivityName ?: "" + withRecordScreenViews { + analytics.screen { + screenName(activityName) + this.category(name) + this.screenProperties { + add(arguments ?: mapOf()) + } + } + } + } + + private fun withTrackLifeCycle(body: () -> Unit) { + if (analytics.currentConfigurationAndroid?.trackLifecycleEvents == true) { + body() + } + } + + private fun withRecordScreenViews(body: () -> Unit) { + if (analytics.currentConfigurationAndroid?.recordScreenViews == true) { + body() + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt new file mode 100644 index 000000000..8dbd23d52 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ReinstatePlugin.kt @@ -0,0 +1,188 @@ +package com.rudderstack.android.internal.infrastructure + +import com.rudderstack.android.AndroidUtils +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.utilities.androidStorage +import com.rudderstack.android.utilities.contextState +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.android.utilities.initializeSessionManagement +import com.rudderstack.android.utilities.processNewContext +import com.rudderstack.android.utilities.setAnonymousId +import com.rudderstack.core.Analytics +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.models.createContext + +/** + * This plugin is used to reinstate the cache data in V2 SDK. In case no + * cached data preset for v2, we check if the sourceId is present in the V1 + * cached server config, then the data is migrated to V2 SDK Servers the + * following purposes: Reinstate anonymous id, or create new one if absent + * Reinstate user id Initialise session management for the user Reinstate + * traits and external ids + */ +internal class ReinstatePlugin : InfrastructurePlugin { + + override lateinit var analytics: Analytics + + override fun setup(analytics: Analytics) { + super.setup(analytics) + setReinstated(false) + reinstate() + } + + private fun setReinstated(isReinstated: Boolean) { + synchronized(this) { + if (isReinstated) { + analytics.applyInfrastructureClosure { + if (this is DataUploadService) + this.resume() + } + } else { + analytics.applyInfrastructureClosure { + if (this is DataUploadService) + this.pause() + } + } + } + } + + private fun reinstate() { + if (isV2DataAvailable()) { + reinstateV2FromCache() + setReinstated(true) + return + } + migrateV1DataIfAvailable() + if (analytics.currentConfigurationAndroid?.anonymousId == null) { + analytics.currentConfigurationAndroid?.fillDefaults() + setReinstated(true) + return + } + } + + private fun ConfigurationAndroid.fillDefaults() { + analytics?.setAnonymousId(AndroidUtils.generateAnonymousId(collectDeviceId, application)) + analytics?.initializeSessionManagement( + analytics?.androidStorage?.sessionId, + analytics?.androidStorage?.lastActiveTimestamp + ) + } + + private fun reinstateV2FromCache() { + val anonId = analytics.androidStorage.anonymousId ?: analytics.currentConfigurationAndroid?.let { + AndroidUtils.generateAnonymousId( + it.collectDeviceId, + it.application + ) + } + val context = analytics.androidStorage.context + context?.let { + analytics.processNewContext(context) + } + if (anonId != null) + analytics.setAnonymousId(anonId) + analytics.initializeSessionManagement( + analytics.androidStorage.sessionId, analytics.androidStorage.lastActiveTimestamp + ) + } + + private fun isV2DataAvailable(): Boolean { + return !analytics.androidStorage.anonymousId.isNullOrEmpty() || + !analytics.androidStorage.userId.isNullOrEmpty() || + !analytics.contextState?.value.isNullOrEmpty() + } + + private fun migrateV1DataIfAvailable() { + // migrate user id/ anon id + analytics.setUserIdFromV1() + analytics.migrateAnonymousIdFromV1() + analytics.migrateOptOutFromV1() + analytics.migrateContextFromV1() + //we do not store v1 advertising id + analytics.androidStorage.resetV1AdvertisingId() + analytics.migrateSession() + analytics.migrateV1LifecycleProperties() + analytics.androidStorage.migrateV1StorageToV2 { + setReinstated(true) + } + analytics.androidStorage.deleteV1SharedPreferencesFile() + analytics.androidStorage.deleteV1ConfigFiles() + } + + private fun Analytics.migrateSession() { + initializeSessionManagement( + androidStorage.v1SessionId, + androidStorage.v1LastActiveTimestamp + ) + resetV1SessionValues() + } + + private fun Analytics.migrateV1LifecycleProperties() { + migrateV1Build() + migrateV1Version() + } + + private fun Analytics.setUserIdFromV1() { + val traits = androidStorage.v1Traits + val userId = traits?.get("userId") as? String ?: traits?.get("id") as? String + if (userId.isNullOrEmpty() || !this.androidStorage.userId.isNullOrEmpty()) return + androidStorage.setUserId(userId) + } + + private fun Analytics.migrateAnonymousIdFromV1() { + currentConfigurationAndroid?.apply { + (androidStorage.v1AnonymousId + ?: getV1AnonymousIdFromTraits() + ?: AndroidUtils.generateAnonymousId( + collectDeviceId, application + )).let { + logger.error(log = "Unable to migrate anonymousId from V1. Generating new anonymousId") + analytics.setAnonymousId(it) + } + androidStorage.resetV1AnonymousId() + } + } + + private fun Analytics.getV1AnonymousIdFromTraits(): String? { + val traits = androidStorage.v1Traits + return traits?.get("anonymousId") as? String + } + + private fun Analytics.migrateV1Build() { + androidStorage.v1Build?.let { + androidStorage.setBuild(it) + } + androidStorage.resetV1Build() + } + + private fun Analytics.resetV1SessionValues() { + androidStorage.resetV1SessionId() + androidStorage.resetV1SessionLastActiveTimestamp() + } + + private fun Analytics.migrateV1Version() { + androidStorage.v1VersionName?.let { + androidStorage.setVersionName(it) + } + androidStorage.resetV1Version() + } + + + private fun Analytics.migrateOptOutFromV1() { + val optOut = androidStorage.v1OptOut + if (!optOut || this.androidStorage.isOptedOut) return //only relevant if optout is true + analytics.optOut(true) + androidStorage.resetV1OptOut() + } + + private fun Analytics.migrateContextFromV1() { + createContext( + traits = androidStorage.v1Traits, externalIds = androidStorage.v1ExternalIds + ).let { + processNewContext(it) + androidStorage.resetV1Traits() + androidStorage.resetV1ExternalIds() + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt new file mode 100644 index 000000000..dc058bbee --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPlugin.kt @@ -0,0 +1,25 @@ +package com.rudderstack.android.internal.infrastructure + +import com.rudderstack.android.utilities.contextState +import com.rudderstack.android.utilities.processNewContext +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.models.createContext +import com.rudderstack.core.models.updateWith + +class ResetImplementationPlugin : InfrastructurePlugin { + + override lateinit var analytics: Analytics + + private val contextState + get() = analytics.contextState + + override fun reset() { + analytics.processNewContext( + contextState?.value?.updateWith( + traits = mapOf(), + externalIds = listOf() + ) ?: createContext() + ) + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt new file mode 100644 index 000000000..d84a2bc97 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/plugins/ExtractStatePlugin.kt @@ -0,0 +1,144 @@ +package com.rudderstack.android.internal.plugins + +import com.rudderstack.android.utilities.androidStorage +import com.rudderstack.android.utilities.contextState +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.android.utilities.empty +import com.rudderstack.android.utilities.processNewContext +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.models.optAddContext +import com.rudderstack.core.models.traits +import com.rudderstack.core.models.updateWith +import com.rudderstack.core.optAdd + +/** + * Mutates the system state, if required, based on the Event. + * In case of [IdentifyMessage], it is expected to save the traits provided. + */ +internal class ExtractStatePlugin : Plugin { + + override lateinit var analytics: Analytics + + override fun intercept(chain: Plugin.Chain): Message { + val message = chain.message() + + if (!(message is IdentifyMessage || message is AliasMessage)) { + return chain.proceed(message) + } + // alias message can change user id permanently + //save and update traits + //update userId + //save and update external ids + (message.context ?: return message).let { + if (it.traits?.get("anonymousId") == null) + analytics.currentConfigurationAndroid?.anonymousId.let { anonId -> + it.updateWith(traits = it.traits optAdd ("anonymousId" to anonId)) + } else it + }.let { + + //alias and identify messages are expected to contain user id. + //We check in context as well as context.traits with either keys "userId" and "id" + //user id can be retrieved if put directly in context or context.traits with the + //aforementioned ids + val newUserId = getUserId(message) + + analytics.logger.debug(log = "New user id detected: $newUserId") + val prevId = analytics.androidStorage.userId ?: analytics.currentConfigurationAndroid?.anonymousId ?: String.empty() + // in case of identify, the stored traits (if any) are replaced by the ones provided + // if user id is different. else traits are added to it + val msg = when (message) { + is AliasMessage -> { + // in case of alias, we change the user id in traits + newUserId?.let { newId -> + updateNewAndPrevUserIdInContext( + newId, it + ) + }?.let { + replaceContext(it) + message.copy(context = it, userId = newUserId, previousId = prevId) + } + } + + is IdentifyMessage -> { + val updatedContext = if (newUserId != prevId) { + it + } else { + appendContextForIdentify(it) + } + replaceContext(updatedContext) + message.copy(context = updatedContext) + } + + else -> { + message + } + } ?: message + msg.also { + newUserId?.let { id -> + analytics.androidStorage.setUserId(id) + } + } + return chain.proceed(msg) + + } + } + + private fun appendContextForIdentify(messageContext: MessageContext): MessageContext { + return analytics.contextState?.value?.let { savedContext -> + messageContext optAddContext savedContext + } ?: messageContext + } + + private fun replaceContext(messageContext: MessageContext) { + analytics.processNewContext(messageContext) + } + + + /** + * Checks in the order + * "user_id" key at root + * "user_id" key at context.traits + * "userId" key at root + * "userId" key at context.traits + * "id" key at root + * "id" key at context.traits + * + * + */ + private fun getUserId(message: Message): String? { + return message.context?.let { + (it[KeyConstants.CONTEXT_USER_ID_KEY] + ?: (it.traits?.get(KeyConstants.CONTEXT_USER_ID_KEY)) + ?: (it[KeyConstants.CONTEXT_USER_ID_KEY_ALIAS]) + ?: (it.traits?.get(KeyConstants.CONTEXT_USER_ID_KEY_ALIAS)) + ?: (it[KeyConstants.CONTEXT_ID_KEY]) + ?: (it.traits?.get(KeyConstants.CONTEXT_ID_KEY)))?.toString() + } ?: message.userId + } + + private fun updateNewAndPrevUserIdInContext( + newUserId: String, messageContext: MessageContext + ): MessageContext { + val newTraits = + messageContext.traits optAdd mapOf( + KeyConstants.CONTEXT_ID_KEY to newUserId, KeyConstants.CONTEXT_USER_ID_KEY to newUserId + ) + + //also in case of alias, user id in context should also change, given it's + // present there + return messageContext.updateWith( + traits = newTraits + ) + } + + object KeyConstants { + const val CONTEXT_USER_ID_KEY = "user_id" + const val CONTEXT_USER_ID_KEY_ALIAS = "userId" + const val CONTEXT_ID_KEY = "id" + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt new file mode 100644 index 000000000..0b0903957 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/plugins/FillDefaultsPlugin.kt @@ -0,0 +1,77 @@ +package com.rudderstack.android.internal.plugins + +import com.rudderstack.android.utilities.androidStorage +import com.rudderstack.android.utilities.contextState +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.MissingPropertiesException +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.* + +/** + * Fill the defaults for a [Message] + * In case a message contains traits, external ids, custom contexts, this will override + * the values in storage for the same. That means if message contains traits + * {a:b, c:d} and saved traits contain {c:e, g:h, i:j}, the resultant will be + * that of the message, i.e {a:b, c:d}. This is applicable to traits, external ids and custom contexts + * The default context will be added irrespectively. + * In case default context contains externalIds/traits/custom contexts that are common with + * message external ids/traits/custom contexts respectively, the values will be amalgamated with + * preference given to those belonging to message, in case keys match. + * This plugin also adds the userId and anonymousId to the message, if not present. + * this plugin also changes the channel for the messages to android + * + */ +internal class FillDefaultsPlugin : Plugin { + + override lateinit var analytics: Analytics + + /** + * Fill default details for [Message] + * If message contains context, this will replace the ones present + * @throws [MissingPropertiesException] if neither of userId or anonymous id is present + */ + @Throws(MissingPropertiesException::class) + private inline fun T.withDefaults(): T { + val anonId = this.anonymousId ?: analytics.currentConfigurationAndroid?.anonymousId + val userId = this.userId ?: analytics.androidStorage.userId + if (anonId == null && userId == null) { + val ex = MissingPropertiesException("Either Anonymous Id or User Id must be present"); + analytics.currentConfigurationAndroid?.logger?.error( + log = "Missing both anonymous Id and user Id. Use settings to update " + "anonymous id in Analytics constructor", + throwable = ex + ) + throw ex + } + //copying top level context to message context + val newContext = + // in case of alias we purposefully remove traits from context + analytics.contextState?.value?.let { + if (this is AliasMessage && this.userId != analytics.androidStorage.userId) it.updateWith( + traits = mapOf() + ) else it + } selectiveReplace context.let { + if (this !is IdentifyMessage) { + // remove any external ids present in the message + // this is in accordance to v1 + it?.withExternalIdsRemoved() + } else it + } + + return (this.copy( + context = newContext, + anonymousId = anonId, + userId = userId + ) as T) + } + + private infix fun MessageContext?.selectiveReplace(context: MessageContext?): MessageContext? { + if (this == null) return context else if (context == null) return this + return this.updateWith(context) + } + + override fun intercept(chain: Plugin.Chain): Message { + val message = chain.message().withDefaults() + return chain.proceed(message) + } +} diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt new file mode 100644 index 000000000..09ce86aa8 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/plugins/PlatformInputsPlugin.kt @@ -0,0 +1,333 @@ +/* + * Creator: Debanjan Chatterjee on 08/07/22, 11:06 AM Last modified: 08/07/22, 11:06 AM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.internal.plugins + +import android.app.Activity +import android.app.Application +import android.bluetooth.BluetoothAdapter +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.net.wifi.WifiManager +import android.os.Build +import android.provider.Settings +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.rudderstack.android.AndroidUtils.getDeviceId +import com.rudderstack.android.AndroidUtils.isOnClassPath +import com.rudderstack.android.AndroidUtils.isTv +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.LifecycleListenerPlugin +import com.rudderstack.android.utilities.applyConfigurationAndroid +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.optAdd +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.atomic.AtomicReference + +/** + * Sets the context specific to Android + * + * @constructor Initiates the values + * + */ +private const val CHANNEL = "mobile" + +internal class PlatformInputsPlugin : Plugin, LifecycleListenerPlugin { + + override lateinit var analytics: Analytics + private val application + get() = analytics.currentConfigurationAndroid?.application + + private var autoCollectAdvertisingId = false + set(value) { + field = value + if (value && _advertisingId.isNullOrEmpty()) application?.collectAdvertisingId() + else if (!value) synchronized(this) { + _advertisingId = null + } + } + private var _collectDeviceId = false + set(value) { + if (field == value) return + field = value + if (value) application?.collectDeviceId() + else synchronized(this) { + _deviceId = null + } + } + + private var _deviceId: String? = null + private var _advertisingId: String? = null + private var _deviceToken: String? = null + + private val _currentActivity: AtomicReference = AtomicReference() + private val currentActivity: Activity? + get() = _currentActivity.get() + + override fun intercept(chain: Plugin.Chain): Message { + val msg = chain.message() + val newMsg = + msg.copy(context = msg.context optAdd application?.defaultAndroidContext()).also { + it.channel = CHANNEL + } + return chain.proceed(newMsg) + } + + override fun setup(analytics: Analytics) { + super.setup(analytics) + analytics.currentConfigurationAndroid?.updateAdvertisingValues() + } + + override fun updateConfiguration(configuration: Configuration) { + if (configuration !is ConfigurationAndroid) return + configuration.updateAdvertisingValues() + _defaultAndroidContext = null + } + + private fun ConfigurationAndroid.updateAdvertisingValues() { + if (!advertisingId.isNullOrEmpty()) { + synchronized(this) { + if (_advertisingId != advertisingId) + _advertisingId = advertisingId + } + } + autoCollectAdvertisingId = autoCollectAdvertId + _collectDeviceId = collectDeviceId + } + + /** + * Overriding advertising id, will disable auto collection if it's on + * + * @param advertisingId + */ + + @VisibleForTesting + internal fun setAdvertisingId(advertisingId: String) { + autoCollectAdvertisingId = false + synchronized(this) { + _advertisingId = advertisingId + } + } + + internal fun putDeviceToken(deviceToken: String) { + synchronized(this) { + _deviceToken = deviceToken + } + } + + private fun Application.collectAdvertisingId() { + if (!isOnClassPath("com.google.android.gms.ads.identifier.AdvertisingIdClient")) { + analytics.currentConfiguration?.logger?.debug( + log = "Not collecting advertising ID because " + + "com.google.android.gms.ads.identifier.AdvertisingIdClient " + + "was not found on the classpath." + ) + return + } + analytics.currentConfigurationAndroid?.advertisingIdFetchExecutor?.submit { + val adId = try { + getGooglePlayServicesAdvertisingID() + } catch (ex: Exception) { + analytics.currentConfiguration?.logger?.error(log = "Error collecting play services ad id", throwable = ex) + null + } ?: try { + getAmazonFireAdvertisingID() + } catch (ex: Exception) { + analytics.currentConfiguration?.logger?.error(log = "Error collecting amazon fire ad id", throwable = ex) + null + } + analytics.currentConfiguration?.logger?.info(log = "Ad id collected is $adId") + if (adId != null) { + analytics.applyConfigurationAndroid { + copy(advertisingId = adId) + } + } + } + } + + private fun Application.collectDeviceId() { + synchronized(this@PlatformInputsPlugin) { + _deviceId = getDeviceId(this) + } + } + + @Throws(Exception::class) + private fun Application.getGooglePlayServicesAdvertisingID(): String? { + val advertisingInfo = Class.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient") + .getMethod("getAdvertisingIdInfo", Context::class.java).invoke(null, this) + ?: return null + val isLimitAdTrackingEnabled = + advertisingInfo.javaClass.getMethod("isLimitAdTrackingEnabled") + .invoke(advertisingInfo) as? Boolean + if (isLimitAdTrackingEnabled == true) { + analytics.logger.debug(log = "Not collecting advertising ID because isLimitAdTrackingEnabled (Google Play Services) is true.") + return null + } + return advertisingInfo.javaClass.getMethod("getId").invoke(advertisingInfo) as? String + } + + @Throws(Exception::class) + private fun Application.getAmazonFireAdvertisingID(): String? { + + val contentResolver: ContentResolver = contentResolver + val limitAdTracking = Settings.Secure.getInt(contentResolver, "limit_ad_tracking") != 0 + if (limitAdTracking) { + analytics.logger.debug(log = "Not collecting advertising ID because limit_ad_tracking (Amazon Fire OS) is true.") + return null + } + return Settings.Secure.getString( + contentResolver, "advertising_id" + ) + } + + private var _defaultAndroidContext: MessageContext? = null + private fun Application.defaultAndroidContext(): MessageContext { + return synchronized(this) { _defaultAndroidContext } ?: generateDefaultAndroidContext() + } + + private fun Application.generateDefaultAndroidContext() = mapOf( + "app" to getAppDetails(), + "os" to getOsInfo(), + "screen" to getScreenInfo(), + "userAgent" to userAgent, + "locale" to locale, + "device" to getDeviceInfo(), + "network" to getRudderNetwork(), + "timezone" to timeZone + ).also { + synchronized(this) { + _defaultAndroidContext = it + } + } + + private fun Application.getAppDetails(): Any? { + try { + val packageName = packageName + val packageManager = packageManager + val packageInfo = packageManager.getPackageInfo(packageName, 0) + val build = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode.toString() else packageInfo.versionCode.toString() + return mapOf( + "name" to packageInfo.applicationInfo.loadLabel(packageManager).toString(), + "build" to build, + "namespace" to packageName, + "version" to packageInfo.versionName + ) + } catch (ex: PackageManager.NameNotFoundException) { + analytics.currentConfiguration?.logger?.error( + log = "Package Name Not Found", + throwable = ex + ) + } + return null + } + + private fun getOsInfo(): Any? { + return mapOf("name" to "Android", "version" to Build.VERSION.RELEASE) + } + + private fun Application.getScreenInfo(): Any? { + return currentActivity?.resources?.displayMetrics?.let { + mapOf( + "density" to it.densityDpi, + "height" to it.heightPixels, + "width" to it.widthPixels + ) + } + + } + + private fun Application.getDeviceInfo(): Any? { + return ((mapOf( + "id" to _deviceId, + "manufacturer" to Build.MANUFACTURER, + "model" to Build.MODEL, + "name" to Build.DEVICE, + "type" to "Android", + "adTrackingEnabled" to !_advertisingId.isNullOrEmpty() + ) optAdd (if (_deviceToken != null) mapOf("token" to _deviceToken) else null)) optAdd if (_advertisingId != null) mapOf( + "advertisingId" to _advertisingId + ) else null) + } + + private fun Application.getRudderNetwork(): Map { + // carrier name + val carrier = if (!isTv()) { + val telephonyManager = + getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + if (telephonyManager != null) telephonyManager.networkOperatorName else "NA" + } else null + + // wifi enabled + val isWifiEnabled = + try { + (this.getSystemService(Context.WIFI_SERVICE) as? WifiManager)?.isWifiEnabled ?: false + } catch (ex: Exception) { + analytics.currentConfiguration?.logger?.error(log = "Cannot detect wifi. Wifi Permission not available") + false + } + + + // bluetooth + val isBluetoothEnabled = + BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON + + // cellular status + val tm = getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + val isCellularEnabled = + if (tm != null && tm.simState == TelephonyManager.SIM_STATE_READY) { + Settings.Global.getInt(contentResolver, "mobile_data", 1) == 1 + } else false + val networkMap = HashMap() + if (carrier != null) { + networkMap["carrier"] = carrier + } + networkMap["wifi"] = isWifiEnabled + networkMap["bluetooth"] = isBluetoothEnabled + networkMap["cellular"] = isCellularEnabled + return networkMap + + } + + private val userAgent + get() = System.getProperty("http.agent") + private val locale + get() = Locale.getDefault().language + "-" + Locale.getDefault().country + private val timeZone + get() = TimeZone.getDefault().id + + override fun onAppBackgrounded() { + _currentActivity.set(null) + } + + override fun setCurrentActivity(activity: Activity?) { + _currentActivity.set(activity) + _defaultAndroidContext = null + //we generate the default context here because we need the activity to get the screen info + application?.generateDefaultAndroidContext() + } + + override fun onShutDown() { + super.onShutDown() + analytics.currentConfigurationAndroid?.advertisingIdFetchExecutor?.shutdownNow() + } + +} diff --git a/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt b/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt new file mode 100644 index 000000000..f84799e4c --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/plugins/SessionPlugin.kt @@ -0,0 +1,72 @@ +package com.rudderstack.android.internal.plugins + +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.extensions.withSessionId +import com.rudderstack.android.internal.extensions.withSessionStart +import com.rudderstack.android.models.UserSession +import com.rudderstack.android.utilities.defaultLastActiveTimestamp +import com.rudderstack.android.utilities.resetSession +import com.rudderstack.android.utilities.startSessionIfNeeded +import com.rudderstack.android.utilities.updateSessionEnd +import com.rudderstack.android.utilities.userSessionState +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message + +internal class SessionPlugin : Plugin { + + override lateinit var analytics: Analytics + + private var currentConfiguration: ConfigurationAndroid? = null + + override fun updateConfiguration(configuration: Configuration) { + if (configuration !is ConfigurationAndroid) return + if (currentConfiguration?.trackAutoSession == configuration.trackAutoSession + && currentConfiguration?.trackLifecycleEvents == configuration.trackLifecycleEvents + ) return + if (!configuration.trackAutoSession || !configuration.trackLifecycleEvents) { + analytics.updateSessionEnd() + return + } + analytics.startSessionIfNeeded() + } + + + override fun intercept(chain: Plugin.Chain): Message { + //apply session id and session start + // update last active timestamp + // if difference between two events is more than session timeout, refresh session + val message = chain.message() + analytics.startSessionIfNeeded() + val newMsg = analytics.userSessionState?.value?.takeIf { it.isActive }?.let { + updateWithSession(message, it) + } ?: message + + return chain.proceed(newMsg) + } + + private fun updateWithSession( + message: Message, it: UserSession + ): Message { + val context = message.context + var newContext = context?.withSessionId(it.sessionId.toString()) + if (it.sessionStart) newContext = newContext?.withSessionStart(true) + + updateSessionState(it) + return message.copy(newContext) + } + + private fun updateSessionState(it: UserSession) { + val updatedSession = it.copy( + lastActiveTimestamp = defaultLastActiveTimestamp, sessionStart = false + ) + analytics.userSessionState?.update(updatedSession) + } + + + override fun reset() { + analytics.resetSession() + } + +} diff --git a/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt b/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt new file mode 100644 index 000000000..a38d4be54 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/states/ContextState.kt @@ -0,0 +1,8 @@ +package com.rudderstack.android.internal.states + +import com.rudderstack.core.State +import com.rudderstack.core.models.MessageContext + +internal class ContextState : State( + mapOf() +) diff --git a/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt b/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt new file mode 100644 index 000000000..f1d7e8bd2 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/internal/states/UserSessionState.kt @@ -0,0 +1,8 @@ +package com.rudderstack.android.internal.states + +import com.rudderstack.core.State +import com.rudderstack.android.models.UserSession + +internal class UserSessionState : State( + UserSession() +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderApp.kt b/android/src/main/java/com/rudderstack/android/models/RudderApp.kt new file mode 100644 index 000000000..535fd2023 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderApp.kt @@ -0,0 +1,24 @@ +package com.rudderstack.android.models + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json + +data class RudderApp( + @SerializedName("build") + @JsonProperty("build") + @Json(name = "build") + private val build: String, + @SerializedName("name") + @JsonProperty("name") + @Json(name = "name") + private val name: String, + @SerializedName("namespace") + @JsonProperty("namespace") + @Json(name = "namespace") + private val nameSpace: String, + @SerializedName("version") + @JsonProperty("version") + @Json(name = "version") + private val version: String, +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderContext.kt b/android/src/main/java/com/rudderstack/android/models/RudderContext.kt new file mode 100644 index 000000000..008ad220e --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderContext.kt @@ -0,0 +1,29 @@ +package com.rudderstack.android.models + +class RudderContext(contextMap: Map) : HashMap(contextMap) { + constructor() : this(mapOf()) + + var app: RudderApp? by this + + var traits: RudderTraits? by this + + var library: RudderLibraryInfo? by this + + var os: RudderOSInfo? by this + + var screen: RudderScreenInfo? by this + + var userAgent: String? by this + + var locale: String? by this + + var device: RudderDeviceInfo? by this + + var network: RudderNetwork? by this + + var timezone: String? by this + + var externalId: MutableSet>? by this + + var customContextMap: MutableMap? by this +} diff --git a/android/src/main/java/com/rudderstack/android/models/RudderDeviceInfo.kt b/android/src/main/java/com/rudderstack/android/models/RudderDeviceInfo.kt new file mode 100644 index 000000000..b1ef7c40f --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderDeviceInfo.kt @@ -0,0 +1,52 @@ +package com.rudderstack.android.models + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json + +class RudderDeviceInfo( + @SerializedName("id") + @JsonProperty("id") + @Json(name = "id") + val deviceId: String? = null, + + @SerializedName("manufacturer") + @JsonProperty("manufacturer") + @Json(name = "manufacturer") + private val manufacturer: String, + /*= Build.MANUFACTURER*/ + + @SerializedName("model") + @JsonProperty("model") + @Json(name = "model") + private val model: String, + /*= Build.MODEL*/ + + @SerializedName("name") + @JsonProperty("name") + @Json(name = "name") + private val name: String, + /*= Build.DEVICE*/ + + @SerializedName("type") + @JsonProperty("type") + @Json(name = "type") + private val type: String = "Android", + + @SerializedName("token") + @JsonProperty("token") + @Json(name = "token") + private var token: String? = null, + + @SerializedName("adTrackingEnabled") + @JsonProperty("adTrackingEnabled") + @Json(name = "adTrackingEnabled") + var isAdTrackingEnabled: Boolean? = null, +) { + + @SerializedName("advertisingId") + var advertisingId: String? = null + fun setToken(token: String?) { + this.token = token + } +} diff --git a/android/src/main/java/com/rudderstack/android/models/RudderLibraryInfo.kt b/android/src/main/java/com/rudderstack/android/models/RudderLibraryInfo.kt new file mode 100644 index 000000000..d256de758 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderLibraryInfo.kt @@ -0,0 +1,11 @@ +package com.rudderstack.android.models + +import com.google.gson.annotations.SerializedName + +class RudderLibraryInfo( + @SerializedName("name") + private val name: String? = null, + + @SerializedName("version") + private val version: String = "1.2.1", +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderNetwork.kt b/android/src/main/java/com/rudderstack/android/models/RudderNetwork.kt new file mode 100644 index 000000000..08621b9de --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderNetwork.kt @@ -0,0 +1,27 @@ +package com.rudderstack.android.models + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json + +class RudderNetwork( + @SerializedName("carrier") + @JsonProperty("carrier") + @Json(name = "carrier") + private val carrier: String? = null, + + @SerializedName("wifi") + @JsonProperty("wifi") + @Json(name = "wifi") + private val isWifiEnabled: Boolean = false, + + @SerializedName("bluetooth") + @JsonProperty("bluetooth") + @Json(name = "bluetooth") + private val isBluetoothEnabled: Boolean = false, + + @SerializedName("cellular") + @JsonProperty("cellular") + @Json(name = "cellular") + private val isCellularEnabled: Boolean = false, +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderOSInfo.kt b/android/src/main/java/com/rudderstack/android/models/RudderOSInfo.kt new file mode 100644 index 000000000..cd804c6ad --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderOSInfo.kt @@ -0,0 +1,11 @@ +package com.rudderstack.android.models + +import com.google.gson.annotations.SerializedName + +class RudderOSInfo( + @SerializedName("name") + private val name: String = "Android", + + @SerializedName("version") // = Build.VERSION.RELEASE + private val version: String? = null, +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderProperty.kt b/android/src/main/java/com/rudderstack/android/models/RudderProperty.kt new file mode 100644 index 000000000..7f26694db --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderProperty.kt @@ -0,0 +1,51 @@ +package com.rudderstack.android.models + +abstract class RudderProperty { + + private val map: MutableMap = HashMap() + open fun getMap(): Map { + return map + } + + fun hasProperty(key: String): Boolean { + return map.containsKey(key) + } + + fun getProperty(key: String): Any? { + return if (map.containsKey(key)) map[key] else null + } + + fun put(key: String, value: Any) { + map[key] = value + } + + fun putValue(key: String, value: Any): RudderProperty { + if (value is RudderProperty) { + map[key] = value.getMap() + } else { + map[key] = value + } + return this + } + + fun putValue(map: Map?): RudderProperty { + if (map != null) this.map.putAll(map) + return this + } + + fun putRevenue(revenue: Double) { + map["revenue"] = revenue + } + + fun putCurrency(currency: String) { + map["currency"] = currency + } +} + +data class ScreenProperty(private val screenName: String, private val isAutomatic: Boolean = false) : + RudderProperty() { + init { + putValue("name", screenName) + putValue("automatic", isAutomatic) + } +} diff --git a/android/src/main/java/com/rudderstack/android/models/RudderScreenInfo.kt b/android/src/main/java/com/rudderstack/android/models/RudderScreenInfo.kt new file mode 100644 index 000000000..d38df10d9 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderScreenInfo.kt @@ -0,0 +1,22 @@ +package com.rudderstack.android.models + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json + +class RudderScreenInfo( + @SerializedName("density") + @JsonProperty("density") + @Json(name = "density") + private val density: Int = 0, + + @SerializedName("width") + @JsonProperty("width") + @Json(name = "width") + private var width: Int = 0, + + @SerializedName("height") + @JsonProperty("height") + @Json(name = "height") + private var height: Int = 0, +) diff --git a/android/src/main/java/com/rudderstack/android/models/RudderTraits.java b/android/src/main/java/com/rudderstack/android/models/RudderTraits.java new file mode 100644 index 000000000..c5f7ef777 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/RudderTraits.java @@ -0,0 +1,736 @@ +package com.rudderstack.android.models; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class RudderTraits { + @SerializedName("anonymousId") + private String anonymousId; + @SerializedName("address") + private Address address; + @SerializedName("age") + private String age; + @SerializedName("birthday") + private String birthday; + @SerializedName("company") + private Company company; + @SerializedName("createdat") + private String createdAt; + @SerializedName("description") + private String description; + @SerializedName("email") + private String email; + @SerializedName("firstname") + private String firstName; + @SerializedName("gender") + private String gender; + @SerializedName("userId") + private String id; + @SerializedName("id") + private String oldId; + @SerializedName("lastname") + private String lastName; + @SerializedName("name") + private String name; + @SerializedName("phone") + private String phone; + @SerializedName("title") + private String title; + @SerializedName("username") + private String userName; + private Map extras; + + private static final String ANONYMOUSID_KEY = "anonymousId"; + private static final String ADDRESS_KEY = "address"; + private static final String AGE_KEY = "age"; + private static final String BIRTHDAY_KEY = "birthday"; + private static final String COMPANY_KEY = "company"; + private static final String CREATEDAT_KEY = "createdat"; + private static final String DESCRIPTION_KEY = "description"; + private static final String EMAIL_KEY = "email"; + private static final String FIRSTNAME_KEY = "firstname"; + private static final String GENDER_KEY = "gender"; + private static final String USERID_KEY = "userId"; + private static final String LASTNAME_KEY = "lastname"; + private static final String NAME_KEY = "name"; + private static final String PHONE_KEY = "phone"; + private static final String TITLE_KEY = "title"; + private static final String USERNAME_KEY = "username"; + + /** + * Get Anonymous Id from traits + * + * @param traitsMap Map + * @return anonymousId String + */ + public static String getAnonymousId(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(ANONYMOUSID_KEY)) + return (String) traitsMap.get(ANONYMOUSID_KEY); + return null; + } + + /** + * Get Address from traits + * + * @param traitsMap Map + * @return address String + */ + public static String getAddress(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(ADDRESS_KEY)) + return new Gson().toJson(traitsMap.get(ADDRESS_KEY)); + return null; + } + + /** + * Get Age from traits + * + * @param traitsMap Map + * @return age String + */ + public static String getAge(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(AGE_KEY)) + return (String) traitsMap.get(AGE_KEY); + return null; + } + + /** + * Get Birthday from traits + * + * @param traitsMap Map + * @return birthday String + */ + public static String getBirthday(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(BIRTHDAY_KEY)) + return (String) traitsMap.get(BIRTHDAY_KEY); + return null; + } + + /** + * Get Company from traits + * + * @param traitsMap Map + * @return company String + */ + public static String getCompany(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(COMPANY_KEY)) + return (String) traitsMap.get(COMPANY_KEY); + return null; + } + + /** + * Get createdAt from traits + * + * @param traitsMap Map + * @return created_at String + */ + public static String getCreatedAt(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(CREATEDAT_KEY)) + return (String) traitsMap.get(CREATEDAT_KEY); + return null; + } + + /** + * Get description from traits + * + * @param traitsMap Map + * @return description String + */ + public static String getDescription(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(DESCRIPTION_KEY)) + return (String) traitsMap.get(DESCRIPTION_KEY); + return null; + } + + /** + * Get First Name from traits + * + * @param traitsMap Map + * @return firstName String + */ + public static String getFirstname(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(FIRSTNAME_KEY)) + return (String) traitsMap.get(FIRSTNAME_KEY); + return null; + } + + /** + * Get email from traits + * + * @param traitsMap Map + * @return email String + */ + public static String getEmail(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(EMAIL_KEY)) + return (String) traitsMap.get(EMAIL_KEY); + return null; + } + + /** + * Get gender from traits + * + * @param traitsMap Map + * @return gender String + */ + public static String getGender(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(GENDER_KEY)) + return (String) traitsMap.get(GENDER_KEY); + return null; + } + + /** + * Get user id from traits + * + * @param traitsMap Map + * @return userId String + */ + public static String getUserid(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(USERID_KEY)) + return (String) traitsMap.get(USERID_KEY); + return null; + } + + /** + * Get Last Name from traits + * + * @param traitsMap Map + * @return lastName String + */ + public static String getLastname(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(LASTNAME_KEY)) + return (String) traitsMap.get(LASTNAME_KEY); + return null; + } + + /** + * Get name from traits + * + * @param traitsMap Map + * @return name String + */ + public static String getName(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(NAME_KEY)) + return (String) traitsMap.get(NAME_KEY); + return null; + } + + /** + * Get phone from traits + * + * @param traitsMap Map + * @return phone String + */ + public static String getPhone(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(PHONE_KEY)) + return (String) traitsMap.get(PHONE_KEY); + return null; + } + + /** + * Get title from traits + * + * @param traitsMap Map + * @return title String + */ + public static String getTitle(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(TITLE_KEY)) + return (String) traitsMap.get(TITLE_KEY); + return null; + } + + /** + * Get user name from traits + * + * @param traitsMap Map + * @return userName String + */ + public static String getUsername(Map traitsMap) { + if (traitsMap != null & traitsMap.containsKey(USERNAME_KEY)) + return (String) traitsMap.get(USERNAME_KEY); + return null; + } + + + /** + * constructor + */ + /*public RudderTraits() { + Application application = RudderClient.getApplication(); + if (application != null) { + this.anonymousId = RudderContext.getAnonymousId(); + } + }*/ + + + /** + * constructor + * + * @param anonymousId String + */ + public RudderTraits(String anonymousId) { + this.anonymousId = anonymousId; + } + + /** + * Initialise RudderTraits + * + * @param address Address + * @param age String + * @param birthday String + * @param company Company + * @param createdAt String + * @param description String + * @param email String + * @param firstName String + * @param gender String + * @param id String + * @param lastName String + * @param name String + * @param phone String + * @param userName String + * @param title String + */ + public RudderTraits(Address address, String age, String birthday, Company company, String createdAt, String description, String email, String firstName, String gender, String id, String lastName, String name, String phone, String title, String userName) { + /*Application application = RudderClient.getApplication(); + if (application != null) { + this.anonymousId = RudderContext.getAnonymousId(); + }*/ + this.address = address; + this.age = age; + this.birthday = birthday; + this.company = company; + this.createdAt = createdAt; + this.description = description; + this.email = email; + this.firstName = firstName; + this.gender = gender; + this.id = id; + this.oldId = id; + this.lastName = lastName; + this.name = name; + this.phone = phone; + this.title = title; + this.userName = userName; + } + + /** + * Get Id + * + * @return id String + */ + public String getId() { + return id; + } + + /** + * Get Extras + * + * @return map Map + */ + public Map getExtras() { + return extras; + } + + + /** + * Put Address + * + * @param address Address + * @return traits RudderTraits + */ + public RudderTraits putAddress(Address address) { + this.address = address; + return this; + } + + /** + * put Age + * + * @param age String + * @return traits RudderTraits + */ + public RudderTraits putAge(String age) { + this.age = age; + return this; + } + + /** + * put Birthday + * + * @param birthday String + * @return traits RudderTraits + */ + public RudderTraits putBirthday(String birthday) { + this.birthday = birthday; + return this; + } + + /** + * put Birthday as Date + * + * @param birthday Date + * @return traits RudderTraits + */ + public RudderTraits putBirthday(Date birthday) { +// this.birthday = Utils.toDateString(birthday); + return this; + } + + /** + * put Company + * + * @param company Company + * @return traits RudderTraits + */ + public RudderTraits putCompany(Company company) { + this.company = company; + return this; + } + + /** + * put Created At + * + * @param createdAt String + * @return traits RudderTraits + */ + public RudderTraits putCreatedAt(String createdAt) { + this.createdAt = createdAt; + return this; + } + + /** + * put description + * + * @param description String + * @return traits RudderTraits + */ + public RudderTraits putDescription(String description) { + this.description = description; + return this; + } + + /** + * put email + * + * @param email String + * @return traits RudderTraits + */ + public RudderTraits putEmail(String email) { + this.email = email; + return this; + } + + /** + * put First Name + * + * @param firstName String + * @return traits RudderTraits + */ + public RudderTraits putFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + /** + * put gender + * + * @param gender String + * @return traits RudderTraits + */ + public RudderTraits putGender(String gender) { + this.gender = gender; + return this; + } + + /** + * put id + * + * @param id String + * @return traits RudderTraits + */ + public RudderTraits putId(String id) { + this.id = id; + this.oldId = id; + return this; + } + + /** + * put Last Name + * + * @param lastName String + * @return traits RudderTraits + */ + public RudderTraits putLastName(String lastName) { + this.lastName = lastName; + return this; + } + + /** + * put name + * + * @param name String + * @return traits RudderTraits + */ + public RudderTraits putName(String name) { + this.name = name; + return this; + } + + /** + * put phone + * + * @param phone String + * @return traits RudderTraits + */ + public RudderTraits putPhone(String phone) { + this.phone = phone; + return this; + } + + /** + * put title + * + * @param title String + * @return traits RudderTraits + */ + public RudderTraits putTitle(String title) { + this.title = title; + return this; + } + + /** + * put User Name + * + * @param userName String + * @return traits RudderTraits + */ + public RudderTraits putUserName(String userName) { + this.userName = userName; + return this; + } + + /** + * put generic key value pairs + * + * @param key String + * @param value Object + * @return traits RudderTraits + */ + public RudderTraits put(String key, Object value) { + if (this.extras == null) { + this.extras = new HashMap<>(); + } + this.extras.put(key, value); + return this; + } + + public static class Address { + @SerializedName("city") + private String city; + @SerializedName("country") + private String country; + @SerializedName("postalcode") + private String postalCode; + @SerializedName("state") + private String state; + @SerializedName("street") + private String street; + + /** + * default public constructor + */ + public Address() { + } + + /** + * constructor + * + * @param city String + * @param country String + * @param postalCode String + * @param state String + * @param street String + */ + public Address(String city, String country, String postalCode, String state, String street) { + this.city = city; + this.country = country; + this.postalCode = postalCode; + this.state = state; + this.street = street; + } + + /** + * get city + * + * @return city String + */ + public String getCity() { + return city; + } + + /** + * get country + * + * @return country String + */ + public String getCountry() { + return country; + } + + /** + * get postal code + * + * @return postalCode String + */ + public String getPostalCode() { + return postalCode; + } + + /** + * get state + * + * @return state String + */ + public String getState() { + return state; + } + + /** + * get street + * + * @return street String + */ + public String getStreet() { + return street; + } + + /** + * put city + * + * @param city String + * @return address Address + */ + public Address putCity(String city) { + this.city = city; + return this; + } + + /** + * put country + * + * @param country String + * @return address Address + */ + public Address putCountry(String country) { + this.country = country; + return this; + } + + /** + * put postal code + * + * @param postalCode String + * @return address Address + */ + public Address putPostalCode(String postalCode) { + this.postalCode = postalCode; + return this; + } + + /** + * put state String + * + * @param state String + * @return address Address + */ + public Address putState(String state) { + this.state = state; + return this; + } + + /** + * put street String + * + * @param street String + * @return address Address + */ + public Address putStreet(String street) { + this.street = street; + return this; + } + + /** + * make address from String + * + * @param address String + * @return address Address + */ + public static Address fromString(String address) { + return new Gson().fromJson(address, Address.class); + } + } + + public static class Company { + @SerializedName("name") + private String name; + @SerializedName("id") + private String id; + @SerializedName("industry") + private String industry; + + /** + * default public constructor + */ + public Company() { + } + + /** + * constructor + * + * @param name String + * @param id String + * @param industry String + */ + Company(String name, String id, String industry) { + this.name = name; + this.id = id; + this.industry = industry; + } + + /** + * put name + * + * @param name String + * @return company Company + */ + public Company putName(String name) { + this.name = name; + return this; + } + + /** + * put company Id + * + * @param id String + * @return company Company + */ + public Company putId(String id) { + this.id = id; + return this; + } + + /** + * put industry + * + * @param industry String + * @return company Company + */ + public Company putIndustry(String industry) { + this.industry = industry; + return this; + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/models/UserSession.kt b/android/src/main/java/com/rudderstack/android/models/UserSession.kt new file mode 100644 index 000000000..2e8dc2675 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/models/UserSession.kt @@ -0,0 +1,9 @@ +package com.rudderstack.android.models + +data class UserSession( + val lastActiveTimestamp: Long = -1L, + val sessionId: Long = -1L, + val isActive: Boolean = false, + // signifies a new session has started. should be sent as true only once at start + val sessionStart: Boolean = false +) diff --git a/android/src/main/java/com/rudderstack/android/storage/AndroidStorage.kt b/android/src/main/java/com/rudderstack/android/storage/AndroidStorage.kt new file mode 100644 index 000000000..efb0c87ca --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/AndroidStorage.kt @@ -0,0 +1,75 @@ +package com.rudderstack.android.storage + +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageContext + +interface AndroidStorage : Storage { + val v1OptOut: Boolean + val anonymousId: String? + val userId: String? + val sessionId: Long? + val lastActiveTimestamp: Long? + val advertisingId: String? + val v1AnonymousId: String? + val v1SessionId: Long? + val v1LastActiveTimestamp: Long? + val v1Traits: Map? + val v1ExternalIds: List>? + val v1AdvertisingId: String? + val trackAutoSession: Boolean + val build: Int? + val v1Build: Int? + val versionName: String? + val v1VersionName: String? + /** + * Platform specific implementation of caching context. This can be done locally too. + * + * @param context A map representing the context. Refer to [Message] + */ + fun cacheContext(context: MessageContext) + + + /** + * Retrieve the cached context + */ + val context: MessageContext? + fun setAnonymousId(anonymousId: String) + fun setUserId(userId: String) + + fun setSessionId(sessionId: Long) + fun setTrackAutoSession(trackAutoSession : Boolean) + fun saveLastActiveTimestamp(timestamp: Long) + fun saveAdvertisingId(advertisingId: String) + fun clearSessionId() + fun clearLastActiveTimestamp() + fun resetV1AnonymousId() + fun resetV1OptOut() + fun resetV1Traits() + fun resetV1ExternalIds() + fun resetV1AdvertisingId() + + fun resetV1Build() + fun resetV1Version() + fun resetV1SessionId() + fun resetV1SessionLastActiveTimestamp() + fun setBuild(build: Int) + fun setVersionName(versionName: String) + + /** + * Migrate the v1 database to current v2 database + * + * @return true if v1 database exists else false + */ + fun migrateV1StorageToV2Sync() : Boolean + + /** + * Migrate the v1 database to current v2 database on a separate executor + * + * @param callback Callback with true if v1 database exists else false + */ + fun migrateV1StorageToV2(callback: (Boolean) -> Unit) + + fun deleteV1SharedPreferencesFile() + fun deleteV1ConfigFiles() +} diff --git a/android/src/main/java/com/rudderstack/android/storage/AndroidStorageImpl.kt b/android/src/main/java/com/rudderstack/android/storage/AndroidStorageImpl.kt new file mode 100644 index 000000000..7454e1234 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/AndroidStorageImpl.kt @@ -0,0 +1,469 @@ +package com.rudderstack.android.storage + +import android.app.Application +import android.os.Build +import com.rudderstack.android.BuildConfig +import com.rudderstack.android.internal.RudderPreferenceManager +import com.rudderstack.android.repository.Dao +import com.rudderstack.android.repository.RudderDatabase +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Logger +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import java.util.LinkedList +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicLong + +private const val DB_VERSION = 1 +private const val SERVER_CONFIG_FILE_NAME = "RudderServerConfig" + +private const val CONTEXT_FILE_NAME = "pref_context" +private const val V1_RUDDER_FLUSH_CONFIG_FILE_NAME = "RudderFlushConfig" + +class AndroidStorageImpl( + private val application: Application, + private val useContentProvider: Boolean = false, + private val writeKey: String, + private val storageExecutor: ExecutorService = Executors.newSingleThreadExecutor() +) : AndroidStorage { + + override lateinit var analytics: Analytics + + private var logger: Logger? = null + private val dbName get() = "rs_persistence_$writeKey" + private var jsonAdapter: JsonAdapter? = null + + private var preferenceManager: RudderPreferenceManager? = null + + private val serverConfigFileName + get() = "$SERVER_CONFIG_FILE_NAME-{$writeKey}" + private val contextFileName + get() = "$CONTEXT_FILE_NAME-{$writeKey}" + + private var messageDao: Dao? = null + + private var _storageCapacity: Int = Storage.MAX_STORAGE_CAPACITY //default 2_000 + private var _maxFetchLimit: Int = Storage.MAX_FETCH_LIMIT + private var _storageListeners = setOf() + + private var _backPressureStrategy = Storage.BackPressureStrategy.Drop + + //this holds the count of rows in DB, this helps with the back pressure strategy + //we update this once on init and then on data change listener + private val _dataCount = AtomicLong() + + private val _optOutTime = AtomicLong(0L) + private val _optInTime = AtomicLong(System.currentTimeMillis()) + + private var _anonymousId: String? = null + private var _userId: String? = null + + /** + * This queue holds the messages that are generated prior to destinations waking up + */ + private val startupQ = LinkedList() + + private var _cachedContext: MessageContext? = null + + private var rudderDatabase: RudderDatabase? = null + + override fun setup(analytics: Analytics) { + super.setup(analytics) + initDb(analytics) + preferenceManager = RudderPreferenceManager(application, writeKey) + } + + //message table listener + private val _messageDataListener = object : Dao.DataChangeListener { + override fun onDataInserted(inserted: List) { + onDataChange() + } + + override fun onDataDeleted(deleted: List) { + onDataChange() + } + + private fun onDataChange() { + messageDao?.getCount { + _dataCount.set(it) + _storageListeners.forEach { + it.onDataChange() + } + } + } + } + + private fun initDb(analytics: Analytics) { + rudderDatabase = RudderDatabase( + application, + dbName, + RudderEntityFactory(analytics.currentConfiguration?.jsonAdapter), + useContentProvider, + DB_VERSION, + providedExecutorService = storageExecutor + ) + messageDao = rudderDatabase?.getDao(MessageEntity::class.java, storageExecutor) + messageDao?.addDataChangeListener(_messageDataListener) + + } + + override fun updateConfiguration(configuration: Configuration) { + super.updateConfiguration(configuration) + jsonAdapter = configuration.jsonAdapter + logger = configuration.logger + } + + override fun setStorageCapacity(storageCapacity: Int) { + _storageCapacity = storageCapacity + } + + override fun setMaxFetchLimit(limit: Int) { + _maxFetchLimit = limit + } + + override fun saveMessage(vararg messages: Message) { + val block = { currentCount: Long -> + val excessMessages = currentCount + messages.size - _storageCapacity + if (excessMessages > 0) { + when (_backPressureStrategy) { + Storage.BackPressureStrategy.Drop -> { + messages.dropLast(excessMessages.toInt()) + .saveToDb() //count can never exceed storage cap, + //hence excess messages cannot be greater than messages.size + } + + Storage.BackPressureStrategy.Latest -> { + + messageDao?.delete( + "${MessageEntity.ColumnNames.messageId} IN (" + + //COMMAND FOR SELECTING FIRST $excessMessages to be removed from DB + "SELECT ${MessageEntity.ColumnNames.messageId} FROM ${ + MessageEntity + .TABLE_NAME + } " + "ORDER BY ${MessageEntity.ColumnNames.updatedAt} LIMIT $excessMessages)", + null + ) { + //check messages exceed storage cap + (if (messages.size > _storageCapacity) { + messages.drop(messages.size - _storageCapacity) + } else messages.toList()).saveToDb() + } + } + } + } else messages.toList().saveToDb() + + } + if (_dataCount.get() > 0) { + block.invoke(_dataCount.get()) + } else { + messageDao?.getCount { + _dataCount.set(it) + block.invoke(it) + } + } + } + + private fun List.saveToDb() { + val jsonAdapter = jsonAdapter ?: return + map { + MessageEntity(it, jsonAdapter) + }.apply { + with(messageDao ?: return) { + insert(conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_REPLACE) { it -> + } + } + } + } + + override fun setBackpressureStrategy(strategy: Storage.BackPressureStrategy) { + _backPressureStrategy = strategy + } + + override fun deleteMessages(messages: List) { + with(messageDao ?: return) { + messages.entities.filterNotNull().delete { } + } + } + + override fun addMessageDataListener(listener: Storage.DataListener) { + _storageListeners = _storageListeners + listener + } + + override fun removeMessageDataListener(listener: Storage.DataListener) { + _storageListeners = _storageListeners - listener + } + + override fun getData(offset: Int, callback: (List) -> Unit) { + messageDao?.runGetQuery( + limit = "$offset,$_maxFetchLimit", + orderBy = MessageEntity.ColumnNames.updatedAt + ) { + callback.invoke(it.map { it.message }) + } + } + + override fun getCount(callback: (Long) -> Unit) { + messageDao?.getCount(callback = callback) + } + + override fun getDataSync(offset: Int): List { + return messageDao?.runGetQuerySync( + null, null, null, MessageEntity.ColumnNames.updatedAt, + "$offset,$_maxFetchLimit" + )?.map { + it.message + } ?: listOf() + } + + override fun cacheContext(context: MessageContext) { + context.save() + } + + override val context: MessageContext? + get() = (if (_cachedContext == null) { + _cachedContext = getObject>(application, contextFileName, logger) + _cachedContext + } else _cachedContext) + + //for local caching + private var _serverConfig: RudderServerConfig? = null + override fun saveServerConfig(serverConfig: RudderServerConfig) { + synchronized(this) { + _serverConfig = serverConfig + saveObject( + serverConfig, context = application, serverConfigFileName, logger + ) + } + } + + override val serverConfig: RudderServerConfig? + get() = synchronized(this) { + if (_serverConfig == null) _serverConfig = + getObject(application, serverConfigFileName, logger) + _serverConfig + } + + override fun saveOptOut(optOut: Boolean) { + preferenceManager?.saveOptStatus(optOut) + if (optOut) { + _optOutTime.set(System.currentTimeMillis()) + } else { + _optInTime.set(System.currentTimeMillis()) + } + } + + override fun saveStartupMessageInQueue(message: Message) { + startupQ.add(message) + } + + override fun clearStartupQueue() { + startupQ.clear() + } + + override fun shutdown() { + rudderDatabase?.shutDown() + _dataCount.set(0) + } + + override fun clearStorage() { + startupQ.clear() + messageDao?.delete(null, null) + } + + override fun deleteMessagesSync(messages: List) { + with(messageDao ?: return) { + messages.entities.filterNotNull().deleteSync() + } + } + + override val startupQueue: List + get() = startupQ + override val isOptedOut: Boolean + get() = preferenceManager?.optStatus ?: false + override val optOutTime: Long + get() = _optOutTime.get() + override val optInTime: Long + get() = _optInTime.get() + override val v1OptOut: Boolean + get() = preferenceManager?.v1optOutStatus ?: false + override val anonymousId: String? + get() { + if (_anonymousId == null) { + _anonymousId = preferenceManager?.anonymousId + } + return _anonymousId + } + override val userId: String? + get() { + if (_userId == null) { + _userId = preferenceManager?.userId + } + return _userId + } + override val sessionId: Long? + get() = preferenceManager?.sessionId?.takeIf { it > -1L } + override val lastActiveTimestamp: Long? + get() = preferenceManager?.lastActiveTimestamp?.takeIf { it > -1L } + override val advertisingId: String? + get() = preferenceManager?.advertisingId + override val v1AnonymousId: String? + get() = preferenceManager?.v1AnonymousId + override val v1SessionId: Long? + get() = preferenceManager?.v1SessionId?.takeIf { it > -1L } + override val v1LastActiveTimestamp: Long? + get() = preferenceManager?.v1LastActiveTimestamp?.takeIf { it > -1L } + override val v1Traits: Map? + get() = preferenceManager?.v1Traits?.let { + jsonAdapter?.readJson(it, object : RudderTypeAdapter>() {}) + } + override val v1ExternalIds: List>? + get() = preferenceManager?.v1ExternalIdsJson?.let { + jsonAdapter?.readJson(it, object : RudderTypeAdapter>>() {}) + } + override val v1AdvertisingId: String? + get() = preferenceManager?.v1AdvertisingId + override val trackAutoSession: Boolean + get() = preferenceManager?.trackAutoSession ?: false + override val build: Int? + get() = preferenceManager?.build + override val v1Build: Int? + get() = preferenceManager?.v1Build + override val versionName: String? + get() = preferenceManager?.versionName + override val v1VersionName: String? + get() = preferenceManager?.v1VersionName + + override fun setAnonymousId(anonymousId: String) { + _anonymousId = anonymousId + preferenceManager?.saveAnonymousId(anonymousId) + } + + override fun setUserId(userId: String) { + _userId = userId + preferenceManager?.saveUserId(userId) + } + + override fun setSessionId(sessionId: Long) { + preferenceManager?.saveSessionId(sessionId) + } + + override fun setTrackAutoSession(trackAutoSession: Boolean) { + preferenceManager?.saveTrackAutoSession(trackAutoSession) + } + + override fun saveLastActiveTimestamp(timestamp: Long) { + preferenceManager?.saveLastActiveTimestamp(timestamp) + } + + override fun saveAdvertisingId(advertisingId: String) { + preferenceManager?.saveAdvertisingId(advertisingId) + } + + override fun clearSessionId() { + preferenceManager?.clearSessionId() + } + + override fun clearLastActiveTimestamp() { + preferenceManager?.clearLastActiveTimestamp() + } + + override fun resetV1AnonymousId() { + preferenceManager?.resetV1AnonymousId() + } + + override fun resetV1OptOut() { + preferenceManager?.resetV1OptOut() + } + + override fun resetV1Traits() { + preferenceManager?.resetV1Traits() + } + + override fun resetV1ExternalIds() { + preferenceManager?.resetV1ExternalIds() + } + + override fun resetV1AdvertisingId() { + preferenceManager?.resetV1AdvertisingId() + } + + override fun resetV1Build() { + preferenceManager?.resetV1Build() + } + + override fun resetV1Version() { + preferenceManager?.resetV1VersionName() + } + + override fun resetV1SessionId() { + preferenceManager?.resetV1SessionId() + } + + override fun resetV1SessionLastActiveTimestamp() { + preferenceManager?.resetV1LastActiveTimestamp() + } + + override fun migrateV1StorageToV2Sync(): Boolean { + return migrateV1MessagesToV2Database( + application, + rudderDatabase ?: return false, + jsonAdapter ?: return false, + logger + ) + } + + override fun migrateV1StorageToV2(callback: (Boolean) -> Unit) { + storageExecutor.execute { + callback(migrateV1StorageToV2Sync()) + } + } + + override fun deleteV1SharedPreferencesFile() { + storageExecutor.execute { + preferenceManager?.deleteV1PreferencesFile() + } + } + + override fun deleteV1ConfigFiles() { + storageExecutor.execute { + deleteFile(application, SERVER_CONFIG_FILE_NAME) + deleteFile(application, V1_RUDDER_FLUSH_CONFIG_FILE_NAME) + } + } + + override fun setBuild(build: Int) { + preferenceManager?.saveBuild(build) + } + + override fun setVersionName(versionName: String) { + preferenceManager?.saveVersionName(versionName) + } + + override val libraryName: String + get() = BuildConfig.LIBRARY_PACKAGE_NAME + override val libraryVersion: String + get() = BuildConfig.LIBRARY_VERSION_NAME + override val libraryPlatform: String + get() = "Android" + override val libraryOsVersion: String + get() = Build.VERSION.SDK_INT.toString() + + private val Iterable.entities + get() = map { + it.entity + } + private val Message.entity + get() = jsonAdapter?.let { MessageEntity(this, it) } + + private fun MessageContext.save() { + saveObject( + HashMap(this), application, contextFileName, logger + ) + } +} diff --git a/android/src/main/java/com/rudderstack/android/storage/FileUtils.kt b/android/src/main/java/com/rudderstack/android/storage/FileUtils.kt new file mode 100644 index 000000000..a964f123d --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/FileUtils.kt @@ -0,0 +1,101 @@ +/* + * Creator: Debanjan Chatterjee on 14/02/24, 12:15 pm Last modified: 14/02/24, 12:15 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.storage + +import android.content.Context +import com.rudderstack.android.internal.AndroidLogger +import com.rudderstack.core.Logger +import java.io.FileOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable + +//file access +/** + * Saves a serializable object in file + * + * @param T + * @param obj + * @param context + * @param fileName + * @return + */ +internal fun saveObject( + obj: T, + context: Context, + fileName: String, + logger: Logger? = AndroidLogger() +): Boolean { + try { + val fos: FileOutputStream = context.openFileOutput( + fileName, Context.MODE_PRIVATE + ) + val os = ObjectOutputStream(fos) + os.writeObject(obj) + os.close() + fos.close() + return true + } catch (e: Exception) { + logger?.error( + log = "save object: Exception while saving Object to File", throwable = e + ) + e.printStackTrace() + } + return false +} + +/** + * + * + * @param T + * @param context + * @param fileName + * @return + */ +internal fun getObject( + context: Context, fileName: String, logger: Logger? = AndroidLogger() +): T? { + try { + val file = context.getFileStreamPath(fileName) + if (file != null && file.exists()) { + val fis = context.openFileInput(fileName) + val `is` = ObjectInputStream(fis) + val obj = `is`.readObject() as? T? + `is`.close() + fis.close() + return obj + } + } catch (e: Exception) { + logger?.error( + log = "getObject: Failed to read Object from File", throwable = e + ) + e.printStackTrace() + } + return null +} + +internal fun fileExists(context: Context, filename: String): Boolean { + val file = context.getFileStreamPath(filename) + return file != null && file.exists() +} + +internal fun deleteFile(context: Context, fileName: String): Boolean { + try { + val file = context.deleteFile(fileName) + return true + }catch (ex: Exception){ + return false + } +} diff --git a/android/src/main/java/com/rudderstack/android/storage/MessageEntity.kt b/android/src/main/java/com/rudderstack/android/storage/MessageEntity.kt new file mode 100644 index 000000000..12f6dbbf0 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/MessageEntity.kt @@ -0,0 +1,103 @@ +package com.rudderstack.android.storage + +import android.content.ContentValues +import com.rudderstack.android.internal.STATUS_NEW +import com.rudderstack.android.internal.maskWith +import com.rudderstack.android.repository.Entity +import com.rudderstack.android.repository.annotation.RudderEntity +import com.rudderstack.android.repository.annotation.RudderField +import com.rudderstack.android.storage.MessageEntity.Companion.TABLE_NAME +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.GroupMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.PageMessage +import com.rudderstack.core.models.ScreenMessage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter + +/** + * An [Entity] delegate for [Message] model. + * @constructor creates a [MessageEntity] from a [Message] and [JsonAdapter] + * If this is a legacy message, pass the updatedAt as well, otherwise will be auto generated + */ +@RudderEntity( + TABLE_NAME, [ + RudderField(RudderField.Type.TEXT, MessageEntity.ColumnNames.messageId, primaryKey = true), + RudderField(RudderField.Type.TEXT, MessageEntity.ColumnNames.message), + RudderField(RudderField.Type.INTEGER, MessageEntity.ColumnNames.updatedAt, isIndex = true), + RudderField(RudderField.Type.TEXT, MessageEntity.ColumnNames.type), + RudderField(RudderField.Type.INTEGER, MessageEntity.ColumnNames.status), + ] +) +internal class MessageEntity(val message: Message, + private val jsonAdapter: JsonAdapter, + private val updatedAt: Long? = null) : Entity { + object ColumnNames { + internal const val messageId = "id" + internal const val message = "message" + internal const val updatedAt = "updated" + internal const val type = "type" + internal const val status = "status" + } + val status: Int + get() = _status + private var _status: Int = STATUS_NEW + + fun maskWithDmtStatus(dmtStatus: Int){ + _status = _status.maskWith(dmtStatus) + } + + override fun generateContentValues(): ContentValues { + return ContentValues().also { + it.put(ColumnNames.messageId, message.messageId) + it.put( + ColumnNames.message, + jsonAdapter.writeToJson(message, RudderTypeAdapter {}) + ?.replace("'", BACKLASHES_INVERTED_COMMA) + ) + it.put(ColumnNames.updatedAt, updatedAt?: System.currentTimeMillis()) + it.put(ColumnNames.type, message.getType().value) + it.put(ColumnNames.status, _status) + } + } + + override fun getPrimaryKeyValues(): Array { + return arrayOf(message.messageId) + } + + companion object { + + internal const val TABLE_NAME = "events" + private const val BACKLASHES_INVERTED_COMMA = "\\\\'" + internal fun create( + values: Map, jsonAdapter: JsonAdapter + ): MessageEntity? { + val type = values[ColumnNames.type] as String + val classOfMessage = getClassBasedOnType(type) + val message = jsonAdapter.readJson(values[ColumnNames.message] as String, classOfMessage) + val status = values[ColumnNames.status] as? Int + return MessageEntity( + message ?: return null, + jsonAdapter + ).also {entity -> + status?.let { + entity.maskWithDmtStatus(it) + } + } + } + + private fun getClassBasedOnType(type: String): Class { + return when (type) { + Message.EventType.ALIAS.value -> AliasMessage::class.java + Message.EventType.GROUP.value -> GroupMessage::class.java + Message.EventType.IDENTIFY.value -> IdentifyMessage::class.java + Message.EventType.PAGE.value -> PageMessage::class.java + Message.EventType.SCREEN.value -> ScreenMessage::class.java + Message.EventType.TRACK.value -> TrackMessage::class.java + else -> Message::class.java + } + } + } +} diff --git a/android/src/main/java/com/rudderstack/android/storage/MigrateV1ToV2Utils.kt b/android/src/main/java/com/rudderstack/android/storage/MigrateV1ToV2Utils.kt new file mode 100644 index 000000000..83347b558 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/MigrateV1ToV2Utils.kt @@ -0,0 +1,174 @@ +package com.rudderstack.android.storage + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import com.rudderstack.android.internal.STATUS_CLOUD_MODE_DONE +import com.rudderstack.android.internal.STATUS_DEVICE_MODE_DONE +import com.rudderstack.android.internal.STATUS_NEW +import com.rudderstack.android.internal.isCloudModeDone +import com.rudderstack.android.repository.Entity +import com.rudderstack.android.repository.EntityFactory +import com.rudderstack.android.repository.RudderDatabase +import com.rudderstack.core.Logger +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.GroupMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageIntegrations +import com.rudderstack.core.models.ScreenMessage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import java.util.concurrent.ExecutorService + +private const val V1_DATABASE_NAME = "rl_persistence.db" +private val synchronizeOn = Any() + +/** + * Migrate the V1 events to V2 storage + * + * @param context Android context + * @param v2Database The recent database + * @param jsonAdapter Json adapter to parse events or messages + * @param executorService Passed to V1 database to perform operations, will be shutdown post + * migration + */ +fun migrateV1MessagesToV2Database( + context: Context, + v2Database: RudderDatabase, + jsonAdapter: JsonAdapter, + logger: Logger? = null, + executorService: ExecutorService? = null +) : Boolean{ + logger?.info(log = "Migrating V1 messages to V2 database") + synchronized(synchronizeOn) { + val prevVersion = findPreviousVersion(context).takeIf { it > 0 }?:return false + logger?.debug(log = "Migrating from version: $prevVersion") + val v1Database = RudderDatabase( + context, + V1_DATABASE_NAME, + V1EntityFactory(jsonAdapter), + false, + prevVersion, + providedExecutorService = executorService + ) + val v1MessagesDao = v1Database.getDao(MessageEntity::class.java) + val v1Messages = v1MessagesDao.getAllSync()?.filterNot { + it.status.isCloudModeDone() + }?.takeIf { + it.isNotEmpty() + } ?: run { + v1Database.delete() + v1Database.shutDown() + return true + } + with(v2Database.getDao(MessageEntity::class.java)) { + logger?.info(log = "Migrating ${v1Messages.size} messages") + v1Messages.insertSync() + } + v1Database.delete() + v1Database.shutDown() + } + return true +} + +private fun findPreviousVersion(context: Context): Int { + return try { + val db = SQLiteDatabase.openDatabase( + context.getDatabasePath(V1_DATABASE_NAME).absolutePath, null, + SQLiteDatabase.OPEN_READONLY + ) + val prevVersion = db.version + db.close() + prevVersion + }catch (ex: SQLiteException){ + -1 + } +} + +private const val V1_MESSAGE_ID_COL = "id" +private const val V1_STATUS_COL = "status" + +//status values for database version 2 =. Check createSchema documentation for details. +internal const val V1_STATUS_CLOUD_MODE_DONE = 2 +internal const val V1_STATUS_DEVICE_MODE_DONE = 1 +internal const val V1_STATUS_ALL_DONE = 3 +internal const val V1_STATUS_NEW = 0 + +// This column purpose is to identify if an event is dumped to device mode destinations without transformations or not. +private const val V1_DM_PROCESSED_COL = "dm_processed" + +// status value for DM_PROCESSED column +private const val V1_DM_PROCESSED_PENDING = 0 +private const val V1_DM_PROCESSED_DONE = 1 +const val MESSAGE_COL = "message" +const val UPDATED_COL = "updated" + +class V1EntityFactory(private val jsonAdapter: JsonAdapter) : EntityFactory { + override fun getEntity(entity: Class, values: Map): T? { + return when (entity) { + MessageEntity::class.java -> { + val message = values[MESSAGE_COL] as String + val updatedAt = values[UPDATED_COL] as Long + + val status = when (values[V1_STATUS_COL].toString().toIntOrNull()) { + V1_STATUS_CLOUD_MODE_DONE -> STATUS_CLOUD_MODE_DONE + V1_STATUS_DEVICE_MODE_DONE -> STATUS_DEVICE_MODE_DONE + V1_STATUS_ALL_DONE -> STATUS_CLOUD_MODE_DONE or STATUS_DEVICE_MODE_DONE + else -> STATUS_NEW + } + //TODO - DMT + /*val dmProcessed = when (values[V1_DM_PROCESSED_COL] as Int) { + V1_DM_PROCESSED_PENDING -> false + V1_DM_PROCESSED_DONE -> true + else -> false + }*/ + MessageEntity( + deserializeV1EntityToMessage(message) ?: return null, jsonAdapter, updatedAt + ).also { + it.maskWithDmtStatus(status) } as T + } + + else -> null + } + } + + private fun deserializeV1EntityToMessage(v1EventJson: String): Message? { + val v1EventMap = + jsonAdapter.readJson(v1EventJson, object : RudderTypeAdapter>() {}) + ?: return null + + + val type = v1EventMap["type"] as? String ?: return null + val channel = v1EventMap["channel"] as? String ?: "android" + val integrations = v1EventMap["integrations"] as? MessageIntegrations + return when (type) { + V1MessageType.TRACK -> jsonAdapter.readJson(v1EventJson, TrackMessage::class.java) + + V1MessageType.SCREEN -> jsonAdapter.readJson(v1EventJson, ScreenMessage::class.java) + + V1MessageType.IDENTIFY -> jsonAdapter.readJson(v1EventJson, IdentifyMessage::class.java) + + V1MessageType.ALIAS -> jsonAdapter.readJson(v1EventJson, AliasMessage::class.java) + + V1MessageType.GROUP -> jsonAdapter.readJson(v1EventJson, GroupMessage::class.java) + + + else -> null + }?.also { + it.channel = channel + it.integrations = integrations + } + } + + object V1MessageType { + internal const val TRACK = "track" + internal const val SCREEN = "screen" + internal const val IDENTIFY = "identify" + internal const val ALIAS = "alias" + internal const val GROUP = "group" + } + + +} diff --git a/android/src/main/java/com/rudderstack/android/storage/RudderEntityFactory.kt b/android/src/main/java/com/rudderstack/android/storage/RudderEntityFactory.kt new file mode 100644 index 000000000..cee14fa5a --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/storage/RudderEntityFactory.kt @@ -0,0 +1,31 @@ +/* + * Creator: Debanjan Chatterjee on 28/04/22, 12:26 AM Last modified: 28/04/22, 12:26 AM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.storage + +import com.rudderstack.android.repository.Entity +import com.rudderstack.android.repository.EntityFactory +import com.rudderstack.rudderjsonadapter.JsonAdapter + +internal class RudderEntityFactory(private val jsonAdapter: JsonAdapter?) : EntityFactory { + override fun getEntity(entity: Class, values: Map): T? { + + //we will check the class for conversion + return when(entity){ + MessageEntity::class.java -> jsonAdapter?.let { MessageEntity.create(values, it) as? + T?} + else -> null + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rudderstack/android/utilities/AnalyticsUtil.kt b/android/src/main/java/com/rudderstack/android/utilities/AnalyticsUtil.kt new file mode 100644 index 000000000..7e4f7e044 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/utilities/AnalyticsUtil.kt @@ -0,0 +1,147 @@ +@file:JvmName("AnalyticsUtil") @file:Suppress("FunctionName") + +package com.rudderstack.android.utilities + +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.infrastructure.ActivityBroadcasterPlugin +import com.rudderstack.android.internal.infrastructure.AnonymousIdHeaderPlugin +import com.rudderstack.android.internal.infrastructure.AppInstallUpdateTrackerPlugin +import com.rudderstack.android.internal.infrastructure.LifecycleObserverPlugin +import com.rudderstack.android.internal.infrastructure.ReinstatePlugin +import com.rudderstack.android.internal.infrastructure.ResetImplementationPlugin +import com.rudderstack.android.internal.plugins.ExtractStatePlugin +import com.rudderstack.android.internal.plugins.FillDefaultsPlugin +import com.rudderstack.android.internal.plugins.PlatformInputsPlugin +import com.rudderstack.android.internal.plugins.SessionPlugin +import com.rudderstack.android.internal.states.ContextState +import com.rudderstack.android.internal.states.UserSessionState +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.core.Analytics +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.models.createContext +import com.rudderstack.core.models.traits +import com.rudderstack.core.models.updateWith + +val Analytics.currentConfigurationAndroid: ConfigurationAndroid? + get() = (currentConfiguration as? ConfigurationAndroid) + +internal val Analytics.contextState: ContextState? + get() = retrieveState() +val Analytics.androidStorage: AndroidStorage + get() = (storage as AndroidStorage) + +/** + * Set the AdvertisingId yourself. If set, SDK will not capture idfa + * automatically + * + * @param advertisingId IDFA for the device + */ +fun Analytics.putAdvertisingId(advertisingId: String) { + applyConfiguration { + if (this is ConfigurationAndroid) copy( + autoCollectAdvertId = autoCollectAdvertId && advertisingId.isEmpty(), + advertisingId = advertisingId.takeUnless { it.isEmpty() }?: this.advertisingId + ) + else this + } +} + +/** + * Set the push token for the device to be passed to the downstream + * destinations + * + * @param deviceToken Push Token from FCM + */ +fun Analytics.putDeviceToken(deviceToken: String) { + applyConfiguration { + if (this is ConfigurationAndroid) copy( + deviceToken = deviceToken + ) + else this + } +} + +/** + * Anonymous id to be used for all consecutive calls. Anonymous id is + * mostly used for messages sent prior to user identification or in case of + * anonymous usage. + * + * @param anonymousId String to be used as anonymousId + */ +fun Analytics.setAnonymousId(anonymousId: String) { + androidStorage.setAnonymousId(anonymousId) + applyConfiguration { + if (this is ConfigurationAndroid) copy( + anonymousId = anonymousId + ) + else this + } + val anonymousIdPair = ("anonymousId" to anonymousId) + val newContext = contextState?.value?.let { + it.updateWith(traits = (it.traits ?: mapOf()) + anonymousIdPair) + } ?: createContext(traits = mapOf(anonymousIdPair)) + processNewContext(newContext) +} + +private val infrastructurePlugins = arrayOf( + ReinstatePlugin(), + AnonymousIdHeaderPlugin(), + ActivityBroadcasterPlugin(), + ResetImplementationPlugin() +) + +private val messagePlugins = listOf( + ExtractStatePlugin(), + FillDefaultsPlugin(), + PlatformInputsPlugin(), + SessionPlugin(), + AppInstallUpdateTrackerPlugin(), + LifecycleObserverPlugin(), + ) + +internal fun Analytics.startup() { + associateStates() + addPlugins() +} + + +fun Analytics.applyConfigurationAndroid( + androidConfigurationScope: ConfigurationAndroid.() -> + ConfigurationAndroid +) { + applyConfiguration { + if (this is ConfigurationAndroid) androidConfigurationScope() + else this + } +} + +internal fun Analytics.processNewContext( + newContext: MessageContext +) { + androidStorage.cacheContext(newContext) + contextState?.update(newContext) +} + +internal fun Analytics.onShutdown() { + shutdownSessionManagement() +} +private fun Analytics.addPlugins() { + addInfrastructurePlugin(*infrastructurePlugins) + addPlugin(*messagePlugins.toTypedArray()) +} + +private fun Analytics.associateStates() { + associateState(ContextState()) + attachSavedContextIfAvailable() + associateState(UserSessionState()) +} + +private fun Analytics.attachSavedContextIfAvailable() { + androidStorage.context?.let { + processNewContext(it) + } +} + +fun String.Companion.empty(): String = "" diff --git a/android/src/main/java/com/rudderstack/android/utilities/SessionUtils.kt b/android/src/main/java/com/rudderstack/android/utilities/SessionUtils.kt new file mode 100644 index 000000000..4e85549f9 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/utilities/SessionUtils.kt @@ -0,0 +1,155 @@ +package com.rudderstack.android.utilities + +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.states.UserSessionState +import com.rudderstack.core.Analytics +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.android.models.UserSession +import java.util.concurrent.TimeUnit +import kotlin.math.abs +import kotlin.math.pow + +private val defaultSessionId + get() = TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis() + ) +private const val SESSION_ID_MIN_LENGTH = 10 +internal val defaultLastActiveTimestamp + get() = System.currentTimeMillis() +internal val Analytics.userSessionState : UserSessionState? + get() = retrieveState() +@JvmOverloads +fun Analytics.startSession( + sessionId: Long = defaultSessionId +) { + endSession() + if (!isSessionIdValid(sessionId)) { + currentConfiguration?.logger?.warn( + "Rudderstack User Session", "Invalid session id $sessionId. Must be at least 10 digits" + ) + return + } + if (currentConfigurationAndroid?.trackAutoSession == true) { + applyConfiguration { + if (this is ConfigurationAndroid) copy( + trackAutoSession = false + ) + else this + } + } + updateSessionStart(sessionId) +} + +private fun isSessionIdValid(sessionId: Long): Boolean { + return sessionId / 10.0.pow(SESSION_ID_MIN_LENGTH - 1) >= 1 +} + +fun Analytics.endSession() { + applyConfiguration { + if (this is ConfigurationAndroid) copy( + trackAutoSession = false + ) + else this + } + updateSessionEnd() +} + + +internal fun Analytics.startSessionIfNeeded() { + if (currentConfigurationAndroid?.trackAutoSession != true || currentConfigurationAndroid?.trackLifecycleEvents != true) return + + val currentSession = userSessionState?.value + if (currentSession == null) { + updateSessionStart(defaultSessionId) + return + } + if (!currentSession.isActive || currentSession.lastActiveTimestamp == -1L) { + updateSessionStart(defaultSessionId) + return + } + val timeDifference: Long = synchronized(this) { + abs(System.currentTimeMillis() - currentSession.lastActiveTimestamp) + } + if (timeDifference >= (currentConfigurationAndroid?.sessionTimeoutMillis?.coerceAtLeast(0L) + ?: 0) + ) { + refreshSessionUpdate() + } +} + +internal fun Analytics.initializeSessionManagement(savedSessionId: Long? = null, + lastActiveTimestamp: Long? = null) { + if (currentConfigurationAndroid?.trackAutoSession != true + || currentConfigurationAndroid?.trackLifecycleEvents != true + && androidStorage.trackAutoSession) { + discardAnyPreviousSession() + return + } + + if (savedSessionId != null && lastActiveTimestamp != null) { + userSessionState?.update( + UserSession( + sessionId = savedSessionId, + isActive = true, + lastActiveTimestamp = lastActiveTimestamp + ) + ) + } + startSessionIfNeeded() + listenToSessionChanges() +} + +private fun Analytics.discardAnyPreviousSession() { + updateSessionEnd() +} + +private fun Analytics.listenToSessionChanges() { + userSessionState?.subscribe { newState, _ -> + newState?.apply { + applySessionToStorage(this) + } + } +} + +private fun Analytics.applySessionToStorage( + userSession: UserSession +) { + if (userSession.isActive) { + androidStorage.setSessionId(userSession.sessionId) + androidStorage.saveLastActiveTimestamp(userSession.lastActiveTimestamp) + } else { + androidStorage.clearSessionId() + androidStorage.clearLastActiveTimestamp() + } +} + +internal fun Analytics.shutdownSessionManagement() { + userSessionState?.removeAllObservers() +} + +internal fun Analytics.updateSessionStart(sessionId: Long) { + userSessionState?.update( + UserSession( + sessionId = sessionId, + isActive = true, + lastActiveTimestamp = defaultLastActiveTimestamp, + sessionStart = true + ) + ) +} + +internal fun Analytics.resetSession() { + updateSessionEnd() + startSessionIfNeeded() +} + +internal fun Analytics.updateSessionEnd() { + userSessionState?.update( + UserSession(sessionId = -1L, isActive = false, lastActiveTimestamp = -1L) + ) +} + +internal fun Analytics.refreshSessionUpdate() { + updateSessionEnd() + updateSessionStart(defaultSessionId) +} diff --git a/android/src/main/java/com/rudderstack/android/utilities/V1MigratoryUtils.kt b/android/src/main/java/com/rudderstack/android/utilities/V1MigratoryUtils.kt new file mode 100644 index 000000000..5c8548d52 --- /dev/null +++ b/android/src/main/java/com/rudderstack/android/utilities/V1MigratoryUtils.kt @@ -0,0 +1,55 @@ +/* + * Creator: Debanjan Chatterjee on 14/02/24, 11:14 am Last modified: 14/02/24, 11:14 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.utilities + +import android.content.Context +import com.rudderstack.android.storage.fileExists +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets + +/** + * The methods in this class are used to migrate data from V1 SDK to V2 SDK + */ + +internal fun Context.isV1SavedServerConfigContainsSourceId( + serverConfigFileName: String, newSourceId: String +): Boolean { + //check if v1 source config exists + if (!fileExists(this, serverConfigFileName)) return false + //if exists, read the source id from the file + // it's not possible to read it using ObjectOutputStream as the uid won't match. + //We will try parsing it's byte and check if it contains the source id + try { + openFileInput(serverConfigFileName).use { fis -> + ByteArrayOutputStream().use { outputStream -> + val bufLen = 4 * 0x400 // 4KB + val buf = ByteArray(bufLen) + var readLen: Int + while (fis.read(buf, 0, bufLen).also { readLen = it } != -1) { + outputStream.write(buf, 0, readLen) + } + return String(outputStream.toByteArray(), StandardCharsets.UTF_8).contains( + newSourceId + ) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return false +} + + + diff --git a/android/src/test/java/com/rudderstack/android/AnalyticsRegistryTest.kt b/android/src/test/java/com/rudderstack/android/AnalyticsRegistryTest.kt new file mode 100644 index 000000000..5cb4d00ee --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/AnalyticsRegistryTest.kt @@ -0,0 +1,67 @@ +package com.rudderstack.android + +import com.rudderstack.core.Analytics +import io.mockk.mockk +import org.junit.Before +import org.junit.Test + +class AnalyticsRegistryTest { + + private val writeKey = "writeKey" + private val analytics = mockk() + + @Before + fun setUp() { + AnalyticsRegistry.clear() + } + + @Test + fun `when register is called with a write key and analytics instance, then the instance should be registered`() { + AnalyticsRegistry.register(writeKey, analytics) + + val result = AnalyticsRegistry.getInstance(writeKey) + assert(result == analytics) + } + + @Test + fun `when registering multiple analytics instances with different write keys, then all instances should be registered`() { + val writeKey2 = "writeKey2" + val analytics2 = mockk() + + AnalyticsRegistry.register(writeKey, analytics) + AnalyticsRegistry.register(writeKey2, analytics2) + + val result1 = AnalyticsRegistry.getInstance(writeKey) + val result2 = AnalyticsRegistry.getInstance(writeKey2) + + assert(result1 == analytics) + assert(result2 == analytics2) + } + + @Test + fun `given analytics instance already registered with the writeKey, when register is called with same write key and a new analytics instance, then the instance should not be registered`() { + AnalyticsRegistry.register(writeKey, analytics) + + val newAnalytics = mockk() + AnalyticsRegistry.register(writeKey, newAnalytics) + + val result = AnalyticsRegistry.getInstance(writeKey) + assert(result == analytics) + } + + @Test + fun `given analytics instance with the write key exists, when getInstance is called with that write key, then the Analytics instance should be returned`() { + AnalyticsRegistry.register(writeKey, analytics) + + val result = AnalyticsRegistry.getInstance(writeKey) + assert(result == analytics) + } + + @Test + fun `given analytics instance with the writeKey doesn't exists, when getInstance is called with that unregistered writeKey, then null should be returned`() { + AnalyticsRegistry.register(writeKey, analytics) + + val result = AnalyticsRegistry.getInstance("unRegisteredWriteKey") + assert(result == null) + } +} diff --git a/android/src/test/java/com/rudderstack/android/AndroidStorageTest.kt b/android/src/test/java/com/rudderstack/android/AndroidStorageTest.kt new file mode 100644 index 000000000..75ad1c94e --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/AndroidStorageTest.kt @@ -0,0 +1,233 @@ +package com.rudderstack.android + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.storage.AndroidStorageImpl +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.android.utils.busyWait +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.Storage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import junit.framework.TestSuite +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config +import java.util.Date + +/** + * Test class for testing the AndroidStorageImpl class + */ + +@RunWith(MockitoJUnitRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +abstract class AndroidStorageTest { + private lateinit var analytics: Analytics + lateinit var mockConfig: ConfigurationAndroid + protected abstract val jsonAdapter: JsonAdapter + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + val storage = AndroidStorageImpl( + ApplicationProvider.getApplicationContext(), + false, writeKey = "test_writeKey", + storageExecutor = TestExecutor() + ) + mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = jsonAdapter, + shouldVerifySdk = false, + analyticsExecutor = TestExecutor(), + networkExecutor = TestExecutor(), + flushQueueSize = 200, + maxFlushInterval = 1000, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics = generateTestAnalytics( + mockConfig, storage = storage, + dataUploadService = mock(), configDownloadService = mock() + ) + } + + @After + fun destroy() { + val storage = analytics.storage as AndroidStorageImpl + storage.clearStorage() + analytics.shutdown() + } + + @Test + fun `test drop back pressure strategies`() { + val storage = analytics.storage as AndroidStorageImpl + + storage.clearStorage() + //we check the storage directly + val events = (1..20).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + storage.setStorageCapacity(10) + storage.setBackpressureStrategy(Storage.BackPressureStrategy.Drop) // first 10 will be there + storage.saveMessage(*events.toTypedArray()) + while (storage.getDataSync().size != 10) { + } + //let's busy wait to check if more data is being saved + busyWait(500L) + val first10Events = events.take(10).map { it.eventName } + val last10Events = events.takeLast(10).map { it.eventName } + val saved = storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.allOf( + Matchers.iterableWithSize(10), Matchers.everyItem( + Matchers.allOf( + Matchers.isA(TrackMessage::class.java), Matchers.hasProperty( + "eventName", Matchers.allOf( + Matchers.`in`(first10Events), + Matchers.not(Matchers.`in`(last10Events)) + ) + ) + ) + ) +// contains(last10Events) + ) + ) + } + + @Test + fun `test latest back pressure strategies`() { + val storage = analytics.storage as AndroidStorageImpl + + storage.clearStorage() + val events = (1..20).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + storage.setStorageCapacity(10) + storage.setBackpressureStrategy(Storage.BackPressureStrategy.Latest) // last 10 will be there + + storage.saveMessage(*events.toTypedArray()) + while (storage.getDataSync().size != 10) { + } + busyWait(500L) + val first10Events = events.take(10).map { it.eventName } + val last10Events = events.takeLast(10).map { it.eventName } + MatcherAssert.assertThat( + storage.getDataSync(), Matchers.allOf( + Matchers.iterableWithSize(10), Matchers.everyItem( + Matchers.allOf( + Matchers.isA(TrackMessage::class.java), Matchers.hasProperty( + "eventName", Matchers.allOf( + Matchers.`in`(last10Events), + Matchers.not(Matchers.`in`(first10Events)) + ) + ) + ) + ) +// contains(last10Events) + ) + ) + } + + @Test + fun `test save and retrieve LastActiveTimestamp`() { + val storage = analytics.storage as AndroidStorage + storage.clearStorage() + MatcherAssert.assertThat(storage.lastActiveTimestamp, Matchers.nullValue()) + val timestamp = Date().time + storage.saveLastActiveTimestamp(timestamp) + MatcherAssert.assertThat(storage.lastActiveTimestamp, Matchers.`is`(timestamp)) + storage.clearLastActiveTimestamp() + MatcherAssert.assertThat(storage.lastActiveTimestamp, Matchers.nullValue()) + storage.shutdown() + } + + @Test + fun `test save and retrieve sessionId`() { + val storage = analytics.storage as AndroidStorage + storage.clearStorage() + MatcherAssert.assertThat(storage.sessionId, Matchers.nullValue()) + val sessionId = 123456L + storage.setSessionId(123456L) + MatcherAssert.assertThat(storage.sessionId, Matchers.`is`(sessionId)) + storage.clearSessionId() + MatcherAssert.assertThat(storage.sessionId, Matchers.nullValue()) + } + + @Test + fun `test delete sync`() { + val storage = analytics.storage as AndroidStorage + storage.clearStorage() + val events = (1..20).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + storage.saveMessage(*events.toTypedArray()) + while (storage.getDataSync().size != 20) { + } + storage.deleteMessagesSync(events.take(10)) + MatcherAssert.assertThat(storage.getDataSync(), Matchers.iterableWithSize(10)) + + } + + @Test + fun `when user opts out from tracking, then set optOut to true`() { + val storage = analytics.storage as AndroidStorage + storage.clearStorage() + + MatcherAssert.assertThat(storage.isOptedOut, Matchers.`is`(false)) + storage.saveOptOut(true) + MatcherAssert.assertThat(storage.isOptedOut, Matchers.`is`(true)) + } + + @Test + fun `when user opts in for tracking, then set optOut to false`() { + val storage = analytics.storage as AndroidStorage + storage.clearStorage() + + MatcherAssert.assertThat(storage.isOptedOut, Matchers.`is`(false)) + storage.saveOptOut(true) + MatcherAssert.assertThat(storage.isOptedOut, Matchers.`is`(true)) + storage.saveOptOut(false) + MatcherAssert.assertThat(storage.isOptedOut, Matchers.`is`(false)) + } +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class GsonStorageTest : AndroidStorageTest() { + override val jsonAdapter: JsonAdapter + get() = GsonAdapter() +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class JacksonStorageTest : AndroidStorageTest() { + override val jsonAdapter: JsonAdapter + get() = JacksonAdapter() +} +/* +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class MoshiStorageTest : AndroidStorageTest() { + override val jsonAdapter: JsonAdapter + get() = MoshiAdapter() +}*/ + +@RunWith(Suite::class) +@Suite.SuiteClasses( + GsonStorageTest::class, JacksonStorageTest::class, /*MoshiStorageTest::class*/ +) +class AndroidStorageTestSuite : TestSuite() {} diff --git a/models/src/test/java/com/rudderstack/models/ExampleUnitTest.kt b/android/src/test/java/com/rudderstack/android/ExampleUnitTest.kt similarity index 87% rename from models/src/test/java/com/rudderstack/models/ExampleUnitTest.kt rename to android/src/test/java/com/rudderstack/android/ExampleUnitTest.kt index 3301fe000..beaa54fc5 100644 --- a/models/src/test/java/com/rudderstack/models/ExampleUnitTest.kt +++ b/android/src/test/java/com/rudderstack/android/ExampleUnitTest.kt @@ -1,5 +1,5 @@ /* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM + * Creator: Debanjan Chatterjee on 28/10/21, 7:56 PM Last modified: 28/10/21, 7:56 PM * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,11 +12,12 @@ * permissions and limitations under the License. */ -package com.rudderstack.android.models +package com.rudderstack.android -import org.junit.Assert.* import org.junit.Test +import org.junit.Assert.* + /** * Example local unit test, which will execute on the development machine (host). * @@ -27,4 +28,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} +} \ No newline at end of file diff --git a/android/src/test/java/com/rudderstack/android/RudderAnalyticsTest.kt b/android/src/test/java/com/rudderstack/android/RudderAnalyticsTest.kt new file mode 100644 index 000000000..1b0862fd0 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/RudderAnalyticsTest.kt @@ -0,0 +1,80 @@ +package com.rudderstack.android + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.RudderAnalytics.Companion.getInstance +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class RudderAnalyticsTest { + val writeKey = "writeKey" + + @Test + fun `when writeKey and configuration is passed, then getInstance should return Analytics instance`() { + val analytics = getInstance( + writeKey, ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + MatcherAssert.assertThat(analytics, Matchers.isA(Analytics::class.java)) + } + + @Test + fun `given that the SDK supports a singleton instance, when an attempt is made to create multiple instance with the different writeKey, then both instances should remain the same`() { + val writeKey2 = "writeKey2" + val analytics = getInstance( + writeKey, ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + val analytics2 = getInstance( + writeKey2, ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + MatcherAssert.assertThat(analytics, Matchers.isA(Analytics::class.java)) + MatcherAssert.assertThat(analytics2, Matchers.isA(Analytics::class.java)) + assert(analytics == analytics2) + } + + @Test + fun `given that the SDK supports a singleton instance, when an attempt is made to create multiple instance with the same writeKey, then both instances should remain the same`() { + val analytics = getInstance( + writeKey, ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + val analytics2 = getInstance( + writeKey, ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + MatcherAssert.assertThat(analytics, Matchers.isA(Analytics::class.java)) + MatcherAssert.assertThat(analytics2, Matchers.isA(Analytics::class.java)) + assert(analytics == analytics2) + } +} diff --git a/android/src/test/java/com/rudderstack/android/compat/ConfigurationAndroidBuilderTest.kt b/android/src/test/java/com/rudderstack/android/compat/ConfigurationAndroidBuilderTest.kt new file mode 100644 index 000000000..1ca79b9b9 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/compat/ConfigurationAndroidBuilderTest.kt @@ -0,0 +1,37 @@ +package com.rudderstack.android.compat + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.core.Logger +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class ConfigurationAndroidBuilderTest { + + @Test + fun `when logLevel DEBUG is passed, then assert that configuration has this logLevel set as a property`() { + val configurationAndroidBuilder = + ConfigurationAndroidBuilder(ApplicationProvider.getApplicationContext(), JacksonAdapter()) + .withDataPlaneUrl("https://rudderstack.com") + .withLogLevel(Logger.LogLevel.DEBUG) + .build() + + assertEquals(configurationAndroidBuilder.logger.level, Logger.LogLevel.DEBUG) + } + + @Test + fun `when no logLevel is passed, then assert that configuration has logLevel set to NONE`() { + val configurationAndroidBuilder = + ConfigurationAndroidBuilder(ApplicationProvider.getApplicationContext(), JacksonAdapter()) + .withDataPlaneUrl("https://rudderstack.com") + .build() + + assertEquals(configurationAndroidBuilder.logger.level, Logger.LogLevel.NONE) + } +} diff --git a/android/src/test/java/com/rudderstack/android/internal/DmtUtilsTest.kt b/android/src/test/java/com/rudderstack/android/internal/DmtUtilsTest.kt new file mode 100644 index 000000000..6db7c5569 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/DmtUtilsTest.kt @@ -0,0 +1,62 @@ +package com.rudderstack.android.internal + +import org.junit.Test +import org.mockito.Mockito.* +import org.hamcrest.CoreMatchers.* +import org.hamcrest.MatcherAssert.* + +class DmtUtilsTest { + + @Test + fun `maskWith should set the specified status bit`() { + val initialStatus = STATUS_NEW + val maskedStatus = initialStatus maskWith STATUS_CLOUD_MODE_DONE + assertThat(maskedStatus, `is`(initialStatus or (1 shl STATUS_CLOUD_MODE_DONE))) + } + + @Test + fun `unmaskWith should clear the specified status bit`() { + val initialStatus = STATUS_NEW maskWith STATUS_CLOUD_MODE_DONE + val unmaskedStatus = initialStatus unmaskWith STATUS_CLOUD_MODE_DONE + assertThat(unmaskedStatus, `is`(STATUS_NEW)) + } + + @Test + fun `isDeviceModeDone should return true if the device mode is done`() { + val status = STATUS_NEW maskWith STATUS_DEVICE_MODE_DONE + assertThat(status.isDeviceModeDone(), `is`(true)) + } + + @Test + fun `isDeviceModeDone should return false if the device mode is not done`() { + val status = STATUS_NEW + assertThat(status.isDeviceModeDone(), `is`(false)) + } + + @Test + fun `isCloudModeDone should return true if the cloud mode is done`() { + val status = STATUS_NEW maskWith STATUS_CLOUD_MODE_DONE + assertThat(status.isCloudModeDone(), `is`(true)) + } + @Test + fun `isCloudModeDone should return true if the cloud mode and device modes are done`() { + val status = STATUS_NEW maskWith STATUS_CLOUD_MODE_DONE maskWith STATUS_DEVICE_MODE_DONE + assertThat(status.isCloudModeDone(), `is`(true)) + } + @Test + fun `isDeviceModeDone should return true if the cloud mode and device modes are done`() { + val status = STATUS_NEW maskWith STATUS_CLOUD_MODE_DONE maskWith STATUS_DEVICE_MODE_DONE + assertThat(status.isDeviceModeDone(), `is`(true)) + } + + @Test + fun `isCloudModeDone should return false if the cloud mode is not done`() { + val status = STATUS_NEW + assertThat(status.isCloudModeDone(), `is`(false)) + } + @Test + fun `isCloudModeDone should return false if the only device mode done`() { + val status = STATUS_NEW maskWith STATUS_DEVICE_MODE_DONE + assertThat(status.isCloudModeDone(), `is`(false)) + } +} \ No newline at end of file diff --git a/android/src/test/java/com/rudderstack/android/internal/extensions/UserSessionExtensionsTest.kt b/android/src/test/java/com/rudderstack/android/internal/extensions/UserSessionExtensionsTest.kt new file mode 100644 index 000000000..6000d8ac2 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/extensions/UserSessionExtensionsTest.kt @@ -0,0 +1,120 @@ +package com.rudderstack.android.internal.extensions + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasKey +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.nullValue +import org.hamcrest.Matchers.sameInstance +import org.junit.Test + + +private const val CONTEXT_SESSION_ID_KEY = "sessionId" +private const val CONTEXT_SESSION_START_KEY = "sessionStart" + +class UserSessionExtensionTest { + @Test + fun `withSessionId should add sessionId to MessageContext`() { + // Arrange + val originalContext = mapOf("key" to "value") + + // Act + val updatedContext = originalContext.withSessionId("123") + + // Assert + assertThat(updatedContext, hasEntry(CONTEXT_SESSION_ID_KEY, "123")) + assertThat(originalContext, not(sameInstance(updatedContext))) + } + + @Test + fun `withSessionStart should add sessionStart to MessageContext`() { + // Arrange + val originalContext = mapOf("key" to "value") + + // Act + val updatedContext = originalContext.withSessionStart(true) + + // Assert + assertThat(updatedContext, hasEntry(CONTEXT_SESSION_START_KEY, true)) + assertThat(originalContext, not(sameInstance(updatedContext))) + } + + @Test + fun `removeSessionContext should remove sessionId and sessionStart from MessageContext`() { + // Arrange + val originalContext = mapOf( + CONTEXT_SESSION_ID_KEY to "123", + CONTEXT_SESSION_START_KEY to true, + "key" to "value" + ) + + // Act + val updatedContext = originalContext.removeSessionContext() + + // Assert + assertThat(updatedContext, not(hasKey(CONTEXT_SESSION_ID_KEY))) + assertThat(updatedContext, not(hasKey(CONTEXT_SESSION_START_KEY))) + assertThat(originalContext, not(sameInstance(updatedContext))) + } + + @Test + fun `removeSessionContext should handle empty MessageContext`() { + // Arrange + val originalContext = emptyMap() + + // Act + val updatedContext = originalContext.removeSessionContext() + + // Assert + assertThat(updatedContext, equalTo(originalContext)) + } + + @Test + fun `sessionId should return the correct value from MessageContext`() { + // Arrange + val context = mapOf(CONTEXT_SESSION_ID_KEY to "123") + + // Act + val result = context.sessionId + + // Assert + assertThat(result, equalTo("123")) + } + + @Test + fun `sessionId should return null if key is not present in MessageContext`() { + // Arrange + val context = mapOf("otherKey" to "value") + + // Act + val result = context.sessionId + + // Assert + assertThat(result, nullValue()) + } + + @Test + fun `sessionStart should return the correct value from MessageContext`() { + // Arrange + val context = mapOf(CONTEXT_SESSION_START_KEY to true) + + // Act + val result = context.sessionStart + + // Assert + assertThat(result, equalTo(true)) + } + + @Test + fun `sessionStart should return null if key is not present in MessageContext`() { + // Arrange + val context = mapOf("otherKey" to "value") + + // Act + val result = context.sessionStart + + // Assert + assertThat(result, nullValue()) + } +} diff --git a/android/src/test/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPluginTest.kt new file mode 100644 index 000000000..831571625 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/infrastructure/AppInstallUpdateTrackerPluginTest.kt @@ -0,0 +1,318 @@ +package com.rudderstack.android.internal.infrastructure + +import android.app.Application +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.storage.AndroidStorageImpl +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class AppInstallUpdateTrackerPluginTest { + + private lateinit var appInstallUpdateTrackerPlugin: AppInstallUpdateTrackerPlugin + private val jsonAdapter: JsonAdapter = GsonAdapter() + private val application: Application = ApplicationProvider.getApplicationContext() + private lateinit var analytics: Analytics + private lateinit var storage: AndroidStorage + + @Before + fun setup() { + appInstallUpdateTrackerPlugin = AppInstallUpdateTrackerPlugin() + } + + @After + fun destroy() { + val storage = analytics.storage as AndroidStorageImpl + storage.clearStorage() + analytics.shutdown() + } + + /** + * Helper function to generate an instance of the analytics object with the given configuration. + */ + private fun generateTestAnalytics(trackLifecycleEvents: Boolean = true): Analytics { + storage = AndroidStorageImpl( + ApplicationProvider.getApplicationContext(), + false, + writeKey = "test_writeKey", + storageExecutor = TestExecutor(), + ) + val mockConfig = ConfigurationAndroid( + application = application, + jsonAdapter = jsonAdapter, + shouldVerifySdk = false, + analyticsExecutor = TestExecutor(), + trackLifecycleEvents = trackLifecycleEvents, + logLevel = Logger.LogLevel.DEBUG, + ) + return generateTestAnalytics(mockConfig, storage = this.storage) + } + + /** + * Helper function to simulate app restart by clearing the storage and shutting down the analytics instance. + */ + private fun simulateAppRestart() { + analytics.storage.clearStorage() + analytics.shutdown() + } + + @Test + fun `when lifecycle is enabled at the first app install, then Application Installed event should be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.allOf( + Matchers.iterableWithSize(1), Matchers.everyItem( + Matchers.allOf( + Matchers.isA(TrackMessage::class.java), + Matchers.hasProperty( + "eventName", Matchers.equalTo("Application Installed") + ), + Matchers.hasProperty( + "properties", Matchers.allOf( + Matchers.hasEntry("version", "1.0.0"), + Matchers.hasEntry("build", 1.0) + ) + ) + ) + ) + ) + ) + } + + @Test + fun `when application is updated, then Application Updated should be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics() + setCurrentVersionNameAndCode("1.0.1", 2) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.allOf( + Matchers.hasItems( + Matchers.allOf( + Matchers.isA(TrackMessage::class.java), + Matchers.hasProperty( + "eventName", Matchers.equalTo("Application Updated") + ), + Matchers.hasProperty( + "properties", Matchers.allOf( + Matchers.hasEntry("previous_version", "1.0.0"), + Matchers.hasEntry("previous_build", 1.0), + Matchers.hasEntry("version", "1.0.1"), + Matchers.hasEntry("build", 2.0) + ) + ) + ) + ) + ) + ) + } + + @Test + fun `when lifecycle is disabled at the first app install, then Application Installed shouldn't be made`() { + analytics = generateTestAnalytics(trackLifecycleEvents = false) + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `given lifecycle was disabled at the first app install, when app is launched again with lifecycle enabled, then Application Installed shouldn't be made`() { + analytics = generateTestAnalytics(trackLifecycleEvents = false) + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics() + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `given lifecycle was enabled at the first app install, when app is launched again with lifecycle disabled, then Application Installed shouldn't be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics(trackLifecycleEvents = false) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `given lifecycle was disabled at the first app install, when app is launched again with lifecycle disabled, then Application Installed shouldn't be made`() { + analytics = generateTestAnalytics(trackLifecycleEvents = false) + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics(trackLifecycleEvents = false) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `when application is updated with lifecycle disabled, then Application Updated shouldn't be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics(trackLifecycleEvents = false) + setCurrentVersionNameAndCode("1.0.0", 2) + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `given application is already updated, when app is launched again with lifecycle disabled, then Application Updated shouldn't be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics() + setCurrentVersionNameAndCode("1.0.0", 2) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics(trackLifecycleEvents = false) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `when version name is changed, then Application Update should not be made`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.0", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics() + setCurrentVersionNameAndCode("1.0.1", 1) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.empty() + ) + } + + @Test + fun `given first time application is updated with lifecycle tracking disabled, when app is updated again with lifecycle tracking enabled, then Application Updated should be made with correct properties`() { + analytics = generateTestAnalytics() + setDefaultVersionNameAndCode() + setCurrentVersionNameAndCode("1.0.1", 1) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics(false) + setCurrentVersionNameAndCode("1.0.2", 2) + appInstallUpdateTrackerPlugin.setup(analytics) + simulateAppRestart() + analytics = generateTestAnalytics() + setCurrentVersionNameAndCode("1.0.3", 3) + appInstallUpdateTrackerPlugin.setup(analytics) + + appInstallUpdateTrackerPlugin.setup(analytics) + + val saved = analytics.storage.getDataSync() + MatcherAssert.assertThat( + saved, Matchers.allOf( + Matchers.hasItems( + Matchers.allOf( + Matchers.isA(TrackMessage::class.java), + Matchers.hasProperty( + "eventName", Matchers.equalTo("Application Updated") + ), + Matchers.hasProperty( + "properties", Matchers.allOf( + Matchers.hasEntry("previous_version", "1.0.2"), + Matchers.hasEntry("previous_build", 2.0), + Matchers.hasEntry("version", "1.0.3"), + Matchers.hasEntry("build", 3.0) + ) + ) + ) + ) + ) + ) + } + + /** + * Helper function to set the version name and build in the Robolectric Application object. + */ + private fun setCurrentVersionNameAndCode(versionName: String, build: Long) { + shadowOf(application.packageManager).getInternalMutablePackageInfo(application.packageName).versionName = versionName + shadowOf(application.packageManager).getInternalMutablePackageInfo(application.packageName).longVersionCode = build + } + + /** + * Helper function to set the default version name and build in the storage object. + */ + private fun setDefaultVersionNameAndCode() { + val storage = analytics.storage as AndroidStorageImpl + storage.setVersionName("") + storage.setBuild(-1) + } +} diff --git a/android/src/test/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPluginTest.kt new file mode 100644 index 000000000..7259328a4 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/infrastructure/LifecycleObserverPluginTest.kt @@ -0,0 +1,215 @@ +package com.rudderstack.android.internal.infrastructure + +import android.app.Application +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.providers.provideAnalytics +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.android.utils.busyWait +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.TrackProperties +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.TestDataUploadService +import com.vagabond.testcommon.assertArgument +import com.vagabond.testcommon.inputVerifyPlugin +import io.mockk.mockk +import io.mockk.verify +import junit.framework.TestCase.assertTrue +import junit.framework.TestSuite +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.aMapWithSize +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.isA +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +abstract class LifecycleObserverPluginTest { + private lateinit var analytics: Analytics + private lateinit var configurationAndroid: ConfigurationAndroid + private var mockControlPlane = mock() + abstract val jsonAdapter: JsonAdapter + + @Before + fun setup() { + val listeners = mutableListOf() + whenever(mockControlPlane.addListener(any(), any())).then { + listeners += it.getArgument(0) + Unit + } + whenever(mockControlPlane.download(any())).then { + val cb = it.getArgument<(success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit>(0) + listeners.forEach { it.onDownloaded(true) } + cb(true, RudderServerConfig(), null) + } + configurationAndroid = ConfigurationAndroid( + application = mock(), + jsonAdapter = jsonAdapter, + analyticsExecutor = TestExecutor() + ) + analytics = provideAnalytics( + writeKey = "any write key", + configuration = configurationAndroid, + storage = mock(), + configDownloadService = mockControlPlane, + dataUploadService = TestDataUploadService(), + ).also { + it.addPlugin(inputVerifyPlugin) + } + } + + @After + fun shutdown() { + analytics.shutdown() + } + + @Test + fun `when app is backgrounded, then flush is called`() { + val analytics = mockk(relaxed = true) + val lifecycleObserverPlugin = LifecycleObserverPlugin() + lifecycleObserverPlugin.setup(analytics) + + lifecycleObserverPlugin.onAppBackgrounded() + + verify { analytics.flush() } + lifecycleObserverPlugin.onShutDown() + } + + @Test + fun `test when app foregrounded first time from_background should be false and contain version`() { + val plugin = LifecycleObserverPlugin({ any() }) + plugin.setup(analytics) + plugin.onAppForegrounded() + busyWait(100) + analytics.assertArgument { input, _ -> + assertThat( + input, allOf( + isA(TrackMessage::class.java), + hasProperty("eventName", `is`(EVENT_NAME_APPLICATION_OPENED)), + hasProperty( + "properties", allOf( + aMapWithSize(2), + ) + ) + ) + ) + } + plugin.onShutDown() + + } + +// @Test +// fun `test when app foregrounded second time from_background should be true and not contain version`() { +// val plugin = LifecycleObserverPlugin({ any() }) +// plugin.setup(analytics) +// plugin.onAppForegrounded() +// plugin.onAppForegrounded() +// analytics.assertArgument { input, _ -> +// assertThat( +// input, allOf( +// isA(TrackMessage::class.java), +// hasProperty("eventName", `is`(EVENT_NAME_APPLICATION_OPENED)), +// hasProperty( +// "properties", allOf( +// hasEntry("from_background", true), +// ) +// ) +// ) +// ) +// } +// plugin.onShutDown() +// } + + @Test + fun `test when app backgrounded event name is Application Backgrounded`() { + val plugin = LifecycleObserverPlugin() + plugin.setup(analytics) + plugin.onAppBackgrounded() + busyWait(100) + analytics.assertArgument { input, _ -> + assertThat( + input, allOf( + isA(TrackMessage::class.java), + hasProperty("eventName", `is`(EVENT_NAME_APPLICATION_STOPPED)) + ) + ) + } + plugin.onShutDown() + } + + @Test + fun `test when elapsed time more than 90 minutes update source config is called`() { + var timeNow = 90 * 60 * 1000L + val getTime = { + timeNow.also { + timeNow += it // 90 minutes passed by + } + } + val plugin = LifecycleObserverPlugin(getTime) + plugin.setup(analytics) + plugin.onAppForegrounded() + // whenever(mockConfigurationAndroid.shouldVerifySdk).thenReturn(true) + assertTrue(configurationAndroid.shouldVerifySdk) + plugin.onAppForegrounded() // after 90 minutes stimulated + // org.mockito.kotlin.verify(mockControlPlane).download(any()) + plugin.onShutDown() + + } + +// @Test +// fun `given automatic screen event is enabled, when automatic screen event is made, then screen event containing default properties is sent`() { +// val plugin = LifecycleObserverPlugin() +// plugin.setup(analytics) +// +// plugin.onActivityStarted("MainActivity") +// +// analytics.assertArgument { input, _ -> +// assertThat( +// input, allOf( +// isA(ScreenMessage::class.java), +// hasProperty("eventName", `is`("Application Opened")), +// ) +// ) +// } +// +// plugin.onShutDown() +// } +} + +class GsonLifecycleObserverPluginTest : LifecycleObserverPluginTest() { + override val jsonAdapter: JsonAdapter + get() = GsonAdapter() + +} + +class JacksonLifecycleObserverPluginTest : LifecycleObserverPluginTest() { + override val jsonAdapter: JsonAdapter + get() = JacksonAdapter() + +} +//not supported yet +/*class MoshiLifecycleObserverPluginTest : LifecycleObserverPluginTest() { + override val jsonAdapter: JsonAdapter + get() = MoshiAdapter() + +}*/ + +@RunWith(Suite::class) +@Suite.SuiteClasses( + GsonLifecycleObserverPluginTest::class, + JacksonLifecycleObserverPluginTest::class, +// MoshiLifecycleObserverPluginTest::class +) +class LifecycleObserverPluginTestSuite : TestSuite() diff --git a/android/src/test/java/com/rudderstack/android/internal/infrastructure/ReinstatePluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/infrastructure/ReinstatePluginTest.kt new file mode 100644 index 000000000..1dee3f374 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/infrastructure/ReinstatePluginTest.kt @@ -0,0 +1,209 @@ +package com.rudderstack.android.internal.infrastructure + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.storage.saveObject +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.android.utils.busyWait +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Logger +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.internal.KotlinLogger +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class ReinstatePluginTest { + private lateinit var analytics: Analytics + private lateinit var config: RudderServerConfig + private lateinit var androidStorage: AndroidStorage + private lateinit var mockControlPlane: ConfigDownloadService + + private lateinit var configurationAndroid: ConfigurationAndroid + private lateinit var plugin: ReinstatePlugin + private lateinit var dummyMessage: Message + private val RUDDER_SERVER_FILE_NAME_V1 = "RudderServerConfig" + private val TEST_SOURCE_ID = "testSourceId" + private val context + get() = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + androidStorage = mock() + mockControlPlane = mock() + whenever(mockControlPlane.download(any())).then { + + it.getArgument<(success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit>( + 0 + ).invoke( + true, config, null + ) + } + dummyMessage = TrackMessage.create("dummyEvent", RudderUtils.timeStamp) + config = RudderServerConfig( + source = RudderServerConfig.RudderServerConfigSource( + TEST_SOURCE_ID, isSourceEnabled = true, + ) + ) + configurationAndroid = ConfigurationAndroid( + application = context, + jsonAdapter = mock(), + shouldVerifySdk = true, + analyticsExecutor = TestExecutor(), + logLevel = Logger.LogLevel.DEBUG, + ) + analytics = createAnalyticsInstance() + `when`(androidStorage.v1VersionName).thenReturn("1.0") + `when`(androidStorage.v1AdvertisingId).thenReturn("v1AdId") + + + plugin = ReinstatePlugin() + plugin.setup(analytics) + } + + private fun createAnalyticsInstance() = generateTestAnalytics( + configurationAndroid, storage = androidStorage, configDownloadService = mockControlPlane + ) + + @After + fun tearDown() { + plugin.shutdown() + analytics.shutdown() + } + + @Test + fun `test migration should be called if v1 data available and v2 unavailable`() { + whenever(androidStorage.anonymousId).thenReturn(null) + whenever(androidStorage.userId).thenReturn(null) + whenever(androidStorage.v1Traits).thenReturn(mapOf()) + whenever(androidStorage.v1ExternalIds).thenReturn(listOf()) + whenever(androidStorage.v1OptOut).thenReturn(false) + + saveObject(TEST_SOURCE_ID, context, RUDDER_SERVER_FILE_NAME_V1, KotlinLogger()) + plugin.updateRudderServerConfig(config) + + busyWait(100) + Mockito.verify(androidStorage, times(1)).migrateV1StorageToV2(any()) + + + } + + @Test + fun `test v1Traits should be called if v1 data available and v2 unavailable`() { + whenever(androidStorage.anonymousId).thenReturn(null) + whenever(androidStorage.userId).thenReturn(null) + whenever(androidStorage.v1Traits).thenReturn(mapOf("userId" to "uid")) + whenever(androidStorage.v1ExternalIds).thenReturn(listOf()) + whenever(androidStorage.v1OptOut).thenReturn(false) + + saveObject(TEST_SOURCE_ID, context, RUDDER_SERVER_FILE_NAME_V1, KotlinLogger()) + plugin.updateRudderServerConfig(config) + + busyWait(100) + Mockito.verify(androidStorage, times(3)).v1Traits + } + + @Test + fun `test v1OptOut should be called if v1 data available and v2 unavailable`() { + whenever(androidStorage.anonymousId).thenReturn(null) + whenever(androidStorage.userId).thenReturn(null) + whenever(androidStorage.v1Traits).thenReturn(mapOf("userId" to "uid")) + whenever(androidStorage.v1ExternalIds).thenReturn(listOf()) + whenever(androidStorage.v1OptOut).thenReturn(false) + + saveObject(TEST_SOURCE_ID, context, RUDDER_SERVER_FILE_NAME_V1, KotlinLogger()) + plugin.updateRudderServerConfig(config) + + busyWait(100) + Mockito.verify(androidStorage, times(1)).v1OptOut + } + + @Test + fun `test anonymous id should be migrated if v1 data available and v2 unavailable`() { + busyWait(100) + Mockito.verify(androidStorage, times(1)).v1AnonymousId + Mockito.verify(androidStorage, times(1)).resetV1AnonymousId() + } + + @Test + fun `test advertising id should not be migrated if v1 data available and v2 unavailable`() { + busyWait(100) + Mockito.verify(androidStorage, times(0)).v1AdvertisingId + Mockito.verify(androidStorage, times(0)).saveAdvertisingId(eq("v1AdId")) + Mockito.verify(androidStorage, times(1)).resetV1AdvertisingId() + } + + @Test + fun `test session id should be migrated if v1 data available and v2 unavailable`() { + busyWait(100) + Mockito.verify(androidStorage, times(1)).v1SessionId + Mockito.verify(androidStorage, times(1)).resetV1SessionId() + Mockito.verify(androidStorage, times(1)).resetV1SessionLastActiveTimestamp() + } + + @Test + fun `test build should be migrated if v1 data available and v2 unavailable`() { + busyWait(100) + Mockito.verify(androidStorage, times(1)).v1Build + Mockito.verify(androidStorage, times(1)).resetV1Build() + Mockito.verify(androidStorage, times(1)).setBuild(any()) + } + + @Test + fun `test version should be migrated if v1 data available and v2 unavailable`() { + busyWait(100) + + Mockito.verify(androidStorage, times(1)).v1VersionName + Mockito.verify(androidStorage, times(1)).setVersionName(eq("1.0")) + Mockito.verify(androidStorage, times(1)).resetV1Version() + } + + @Test + fun `test migration should not be called if v1 data unavailable`() { + whenever(androidStorage.anonymousId).thenReturn(null) + whenever(androidStorage.userId).thenReturn(null) + whenever(androidStorage.v1Traits).thenReturn(mapOf()) + whenever(androidStorage.v1ExternalIds).thenReturn(listOf()) + whenever(androidStorage.v1OptOut).thenReturn(false) + + plugin.updateRudderServerConfig(config) + + busyWait(100) + Mockito.verify(androidStorage, never()).migrateV1StorageToV2Sync() + } + + @Test + fun `test migration should not be called if v2 data available`() { + whenever(androidStorage.anonymousId).thenReturn("anonId") + whenever(androidStorage.userId).thenReturn("userId") + whenever(androidStorage.v1Traits).thenReturn(mapOf()) + whenever(androidStorage.v1ExternalIds).thenReturn(listOf()) + whenever(androidStorage.v1OptOut).thenReturn(false) + saveObject(TEST_SOURCE_ID, context, RUDDER_SERVER_FILE_NAME_V1, KotlinLogger()) + plugin.updateRudderServerConfig(config) + + busyWait(100) + Mockito.verify(androidStorage, never()).migrateV1StorageToV2Sync() + } +} diff --git a/android/src/test/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPluginTest.kt new file mode 100644 index 000000000..eefee07c9 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/infrastructure/ResetImplementationPluginTest.kt @@ -0,0 +1,64 @@ +package com.rudderstack.android.internal.infrastructure + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.android.utilities.contextState +import com.rudderstack.android.internal.states.ContextState +import com.rudderstack.android.storage.AndroidStorageImpl +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.models.createContext +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasEntry +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class ResetImplementationPluginTest{ + + private lateinit var analytics: Analytics + @Before + fun setup(){ + analytics = generateTestAnalytics(Configuration(jsonAdapter = mock (),), + storage = AndroidStorageImpl(ApplicationProvider.getApplicationContext(), + writeKey = "test_writeKey", + storageExecutor = TestExecutor() + )) + analytics.associateState(ContextState()) + } + @After + fun tearDown(){ + analytics.shutdown() + } + @Test + fun testResetImplementationPlugin(){ + val resetImplementationPlugin = ResetImplementationPlugin() + resetImplementationPlugin.setup(analytics) + //given + analytics.contextState?.update(createContext( + traits = mapOf("name" to "Debanjan", "email" to "debanjan@rudderstack.com"), + externalIds = listOf(mapOf("id1" to "v1"), mapOf("id2" to "v2")), + customContextMap = mapOf("customContext1" to mapOf("key1" to "value1", "key2" to "value2")) + )) + //when + resetImplementationPlugin.reset() + //then + assertThat(analytics.contextState?.value, allOf( + hasEntry("traits", emptyMap()), + hasEntry("externalId", emptyList>()), + hasEntry("customContextMap", + mapOf("customContext1" to mapOf("key1" to "value1", "key2" to "value2"))), + )) + analytics.shutdown() + } +} diff --git a/android/src/test/java/com/rudderstack/android/internal/plugins/ReinstatePluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/plugins/ReinstatePluginTest.kt new file mode 100644 index 000000000..e69de29bb diff --git a/android/src/test/java/com/rudderstack/android/internal/plugins/SessionPluginTest.kt b/android/src/test/java/com/rudderstack/android/internal/plugins/SessionPluginTest.kt new file mode 100644 index 000000000..bcca341b1 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/internal/plugins/SessionPluginTest.kt @@ -0,0 +1,318 @@ +package com.rudderstack.android.internal.plugins + +import com.rudderstack.android.internal.states.UserSessionState +import com.rudderstack.android.utilities.defaultLastActiveTimestamp +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.holder.removeState +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.android.models.UserSession +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasKey +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.notNullValue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SessionPluginTest { + // protected abstract val jsonAdapter: JsonAdapter +// private lateinit var analytics: Analytics + private lateinit var sessionPlugin: SessionPlugin + private lateinit var analytics: Analytics + + @Before + fun setUp() { + analytics = generateTestAnalytics(mock()) + sessionPlugin = SessionPlugin() + sessionPlugin.setup(analytics) + analytics.associateState(UserSessionState()) + } + @After + fun tearDown() { + sessionPlugin.onShutDown() + analytics.removeState() + analytics.shutdown() + } + private val userSessionState + get() = analytics.retrieveState() + @Test + fun `test intercept with valid session and null context sessionStart true`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + val sessionTimestamp = defaultLastActiveTimestamp + val sessionId = 1234567890L + userSessionState?.update( + UserSession( + sessionId = sessionId, + sessionStart = true, + isActive = true, + lastActiveTimestamp = sessionTimestamp + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(5), + hasEntry("sessionId", sessionId.toString()), + hasEntry("sessionStart", true), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(false) + ), hasProperty("sessionId", `is`(sessionId)) + ) + ) + } + + @Test + fun `test intercept with valid session and null context sessionStart false`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + val sessionTimestamp = defaultLastActiveTimestamp + val sessionId = 1234567890L + userSessionState?.update( + UserSession( + sessionId = sessionId, + sessionStart = false, + isActive = true, + lastActiveTimestamp = sessionTimestamp + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(4), + hasEntry("sessionId", sessionId.toString()), + not(hasKey("sessionStart")), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(false) + ), hasProperty("sessionId", `is`(sessionId)) + ) + ) + } + @Test + fun `test intercept with valid session and valid context sessionStart false`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp, + traits = mapOf("trait1" to "value1"), externalIds = listOf(mapOf("externalId1" to + "value1")), customContextMap = mapOf("customContext1" to "value1")) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + val sessionTimestamp = defaultLastActiveTimestamp + val sessionId = 1234567890L + userSessionState?.update( + UserSession( + sessionId = sessionId, + sessionStart = false, + isActive = true, + lastActiveTimestamp = sessionTimestamp + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), + hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(4), + hasEntry("traits", mapOf("trait1" to "value1")), + hasEntry("externalId", listOf(mapOf("externalId1" to "value1"))), + hasEntry("customContextMap", mapOf("customContext1" to "value1")), + hasEntry("sessionId", sessionId.toString()), + not(hasKey("sessionStart")), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(false) + ), hasProperty("sessionId", `is`(sessionId)) + ) + ) + } + @Test + fun `test intercept with valid session and valid context sessionStart true`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp, + traits = mapOf("trait1" to "value1"), externalIds = listOf(mapOf("externalId1" to + "value1")), customContextMap = mapOf("customContext1" to "value1")) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + val sessionTimestamp = defaultLastActiveTimestamp + val sessionId = 1234567890L + userSessionState?.update( + UserSession( + sessionId = sessionId, + sessionStart = true, + isActive = true, + lastActiveTimestamp = sessionTimestamp + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), + hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(5), + hasEntry("traits", mapOf("trait1" to "value1")), + hasEntry("externalId", listOf(mapOf("externalId1" to "value1"))), + hasEntry("customContextMap", mapOf("customContext1" to "value1")), + hasEntry("sessionId", sessionId.toString()), + hasEntry("sessionStart", true), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(false) + ), hasProperty("sessionId", `is`(sessionId)) + ) + ) + } + @Test + fun `test intercept with inactive session and null context`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + val sessionTimestamp = defaultLastActiveTimestamp + val sessionId = 1234567890L + userSessionState?.update( + UserSession( + sessionId = sessionId, + sessionStart = true, + isActive = false, + lastActiveTimestamp = sessionTimestamp + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(3), + not(hasKey("sessionId")), + not(hasKey("sessionStart")), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(true) + ), hasProperty("sessionId", `is`(sessionId)), + hasProperty("active", `is`(false)) + ) + ) + } + @Test + fun `test intercept with inactive session and valid context`() { + val timestamp = RudderUtils.timeStamp + val message = TrackMessage.create("testEvent", timestamp, + traits = mapOf("trait1" to "value1"), externalIds = listOf(mapOf("externalId1" to + "value1")), customContextMap = mapOf("customContext1" to "value1")) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + userSessionState?.update( + UserSession( + sessionId = -1L, + sessionStart = false, + isActive = false, + lastActiveTimestamp = -1L + ) + ) + sessionPlugin.intercept(mockChain) + + val updatedMessageCapture = argumentCaptor() + verify(mockChain).proceed(updatedMessageCapture.capture()) + + val updatedMessage = updatedMessageCapture.firstValue + MatcherAssert.assertThat( + updatedMessage, allOf( + notNullValue(), hasProperty("timestamp", notNullValue()), + hasProperty( + "context", allOf( + notNullValue(), Matchers.aMapWithSize(3), + hasEntry("traits", mapOf("trait1" to "value1")), + hasEntry("externalId", listOf(mapOf("externalId1" to "value1"))), + hasEntry("customContextMap", mapOf("customContext1" to "value1")), + not(hasKey("sessionId")), + not(hasKey("sessionStart")), + ) + ) + ) + ) + MatcherAssert.assertThat( + userSessionState?.value, allOf( + notNullValue(), hasProperty( + "sessionStart", `is`(false) + ), hasProperty("sessionId", `is`(-1L)) + ) + ) + } +} diff --git a/android/src/test/java/com/rudderstack/android/plugins/ExtractStatePluginTest.kt b/android/src/test/java/com/rudderstack/android/plugins/ExtractStatePluginTest.kt new file mode 100644 index 000000000..5a9f11e36 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/plugins/ExtractStatePluginTest.kt @@ -0,0 +1,186 @@ +package com.rudderstack.android.plugins + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.plugins.ExtractStatePlugin +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.traits +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class ExtractStatePluginTest { + + @Mock + private lateinit var chain: Plugin.Chain + private lateinit var plugin: ExtractStatePlugin + private lateinit var analytics: Analytics + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this); + analytics = generateTestAnalytics( + ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + anonymousId = "anonymousId", + shouldVerifySdk = false, + logLevel = Logger.LogLevel.DEBUG + ), storage = mock() + ) + plugin = ExtractStatePlugin() + plugin.setup(analytics) + } + + @After + fun destroy() { + plugin.onShutDown() + analytics.shutdown() + } + + @Test + fun `intercept should proceed if message is not IdentifyMessage or AliasMessage`() { + val message = TrackMessage.create("ev", RudderUtils.timeStamp) + `when`(chain.message()).thenReturn(message) + plugin.intercept(chain) + verify(chain).proceed(message) + } + + @Test + fun `intercept should proceed with modified message for IdentifyMessage with anonymousId`() { + val identifyMessage = IdentifyMessage.create( + traits = mapOf("anonymousId" to null, "userId" to "userId"), + timestamp = RudderUtils.timeStamp + ) + `when`(chain.message()).thenReturn(identifyMessage) + + plugin.intercept(chain) + val messageCaptor = argumentCaptor() + verify(chain).proceed(messageCaptor.capture()) + val capturedMessage = messageCaptor.lastValue + MatcherAssert.assertThat(capturedMessage.context?.traits?.get("anonymousId"), Matchers.notNullValue()) + } + + @Test + fun `intercept should update context with new userId for AliasMessage`() { + val aliasMessage = AliasMessage.create(RudderUtils.timeStamp, userId = "newUserId") + `when`(chain.message()).thenReturn(aliasMessage) + plugin.intercept(chain) + val messageCaptor = argumentCaptor() + + verify(chain).proceed(messageCaptor.capture()) + val capturedMessage = messageCaptor.lastValue + assertEquals("newUserId", capturedMessage.userId) + } + + /* @Test + fun `getUserId should return userId from context`() { + `when`(message.context).thenReturn(context) + `when`(context["user_id"]).thenReturn("userId") + + val userId = plugin.getUserId(message) + assertEquals("userId", userId) + } + + @Test + fun `getUserId should return userId from context traits`() { + `when`(message.context).thenReturn(context) + `when`(context.traits).thenReturn(mapOf("user_id" to "userId")) + + val userId = plugin.getUserId(message) + assertEquals("userId", userId) + } + + @Test + fun `getUserId should return userId from message`() { + `when`(message.context).thenReturn(null) + `when`(message.userId).thenReturn("userId") + + val userId = plugin.getUserId(message) + assertEquals("userId", userId) + } + + @Test + fun `appendContext should merge context`() { + val contextState = mock(ContextState::class.java) + `when`(analytics.contextState).thenReturn(contextState) + `when`(contextState.value).thenReturn(context) + val newContext = mock(MessageContext::class.java) + `when`(context optAddContext context).thenReturn(newContext) + + plugin.appendContext(context) + + verify(analytics).processNewContext(newContext) + } + + @Test + fun `replaceContext should replace context`() { + plugin.replaceContext(context) + + verify(analytics).processNewContext(context) + } + + @Test + fun `updateNewAndPrevUserIdInContext should update context with new userId`() { + val newContext = mock(MessageContext::class.java) + `when`(context.updateWith(any())).thenReturn(newContext) + + val updatedContext = plugin.updateNewAndPrevUserIdInContext("newUserId", context) + + assertEquals(newContext, updatedContext) + } + */ + /* @Test + fun `intercept should handle IdentifyMessage with same userId`() { + val identifyMessage = mock(IdentifyMessage::class.java) + `when`(identifyMessage.context).thenReturn(context) + `when`(context.traits).thenReturn(mapOf("userId" to "userId")) + `when`(analytics.currentConfigurationAndroid?.userId).thenReturn("userId") + `when`(chain.message()).thenReturn(identifyMessage) + `when`(context.updateWith(any())).thenReturn(context) + + plugin.intercept(chain) + + verify(chain).proceed(identifyMessage) + } + + @Test + fun `intercept should handle IdentifyMessage with different userId`() { + val identifyMessage = mock(IdentifyMessage::class.java) + `when`(identifyMessage.context).thenReturn(context) + `when`(context.traits).thenReturn(mapOf("userId" to "newUserId")) + `when`(analytics.currentConfigurationAndroid?.userId).thenReturn("oldUserId") + `when`(chain.message()).thenReturn(identifyMessage) + `when`(context.updateWith(any())).thenReturn(context) + + plugin.intercept(chain) + + verify(chain).proceed(messageCaptor.capture()) + val capturedMessage = messageCaptor.value as IdentifyMessage + assertEquals("newUserId", capturedMessage.userId) + }*/ +} diff --git a/android/src/test/java/com/rudderstack/android/plugins/FillDefaultsPluginTest.kt b/android/src/test/java/com/rudderstack/android/plugins/FillDefaultsPluginTest.kt new file mode 100644 index 000000000..80c14f10b --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/plugins/FillDefaultsPluginTest.kt @@ -0,0 +1,139 @@ +package com.rudderstack.android.plugins + +import android.os.Build +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.plugins.FillDefaultsPlugin +import com.rudderstack.android.internal.states.ContextState +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.android.storage.AndroidStorageImpl +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.createContext +import com.rudderstack.core.models.customContexts +import com.rudderstack.core.models.externalIds +import com.rudderstack.core.models.traits +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.assertArgument +import com.vagabond.testcommon.generateTestAnalytics +import com.vagabond.testcommon.testPlugin +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.aMapWithSize +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class FillDefaultsPluginTest { + + private lateinit var analytics: Analytics + lateinit var mockConfig: ConfigurationAndroid + private val fillDefaultsPlugin = FillDefaultsPlugin() + private val jsonAdapter: JsonAdapter = GsonAdapter() + private lateinit var storage: AndroidStorage + + @Before + fun setup() { + storage = AndroidStorageImpl( + getApplicationContext(), + false, + writeKey = "test_writeKey", + storageExecutor = TestExecutor(), + ) + mockConfig = ConfigurationAndroid( + application = getApplicationContext(), + jsonAdapter = jsonAdapter, + anonymousId = "anon_id", + shouldVerifySdk = false, + analyticsExecutor = TestExecutor(), + logLevel = Logger.LogLevel.DEBUG, + ) + analytics = generateTestAnalytics(mockConfig, storage = storage) + analytics.associateState(ContextState()) + fillDefaultsPlugin.setup(analytics) + fillDefaultsPlugin.updateConfiguration(mockConfig) + } + + @After + fun destroy() { + analytics.storage.clearStorage() + analytics.shutdown() + } + + /** + * We intend to test if data is filled in properly + * + */ + @Test + fun `test insertion of defaults`() { + analytics.retrieveState()?.update( + createContext( + traits = mapOf( + "name" to "some_name", "age" to 24 + ), externalIds = listOf( + mapOf("braze_id" to "b_id"), + mapOf("amp_id" to "a_id"), + ), customContextMap = mapOf( + "custom_name" to "c_name" + ) + ) + ) + val message = TrackMessage.create( + "ev-1", RudderUtils.timeStamp, traits = mapOf( + "age" to 31, "office" to "Rudderstack" + ), externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), customContextMap = null + ) + + analytics.testPlugin(fillDefaultsPlugin) + analytics.track(message) + analytics.assertArgument { input, output -> + //check for expected values + assertThat(output?.anonymousId, allOf(notNullValue(), `is`("anon_id"))) + //message context to override + assertThat( + output?.context?.traits, allOf( + notNullValue(), + aMapWithSize(2), + hasEntry("age", 31.0), + hasEntry("office", "Rudderstack"), + ) + ) + assertThat( + output?.context?.customContexts, allOf( + notNullValue(), + aMapWithSize(1), + hasEntry("custom_name", "c_name"), + ) + ) + // track messages shouldn't contain external ids sent inside it. + // but it should have the context values + assertThat( + output?.context?.externalIds, + containsInAnyOrder( + mapOf("braze_id" to "b_id"), + mapOf("amp_id" to "a_id") + ) + + ) + } + } +} diff --git a/android/src/test/java/com/rudderstack/android/plugins/PlatformInputsPluginTest.kt b/android/src/test/java/com/rudderstack/android/plugins/PlatformInputsPluginTest.kt new file mode 100644 index 000000000..33c9739a9 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/plugins/PlatformInputsPluginTest.kt @@ -0,0 +1,221 @@ +package com.rudderstack.android.plugins + +import android.app.Application +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.plugins.PlatformInputsPlugin +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.aMapWithSize +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasKey +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config +import java.util.TimeZone + + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29], application = AndroidContextPluginTestApplication::class) +class PlatformInputsPluginTest { + private lateinit var platformInputsPlugin: PlatformInputsPlugin + protected var jsonAdapter: JsonAdapter = JacksonAdapter() + private lateinit var analytics: Analytics + + @Before + fun setUp() { + val app = getApplicationContext() + platformInputsPlugin = PlatformInputsPlugin() + analytics = generateTestAnalytics( + ConfigurationAndroid( + application = app, + jsonAdapter = jsonAdapter, + shouldVerifySdk = false, + logLevel = Logger.LogLevel.DEBUG, + ) + ) + platformInputsPlugin.setup(analytics) + } + + @After + fun destroy() { + platformInputsPlugin.reset() + analytics.shutdown() + } + + @Test + fun testInterceptWithMessage() { + + val message = TrackMessage.create( + "testEvent", + timestamp = RudderUtils.timeStamp, + traits = mapOf("traitKey" to "traitValue") + ) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + whenever(mockChain.proceed(any())).thenAnswer { + it.arguments[0] as TrackMessage + } + val verifyMsg = platformInputsPlugin.intercept(mockChain) + assertThat( + verifyMsg.context, + allOf( + Matchers.aMapWithSize(11), + hasEntry("traits", mapOf("traitKey" to "traitValue")),//yo + hasKey("screen"), + hasEntry("timezone", (TimeZone.getDefault().id)) + ), + ) + assertThat( + verifyMsg.context!!["app"], `is`( + mapOf( + "name" to AndroidContextPluginTestApplication.PACKAGE_NAME, + "build" to "1", "namespace" to AndroidContextPluginTestApplication.PACKAGE_NAME, + "version" to "1.0" + ) + ) + ) + assertThat( + verifyMsg.context!!["os"] as Map<*, *>, `is`( + allOf( + aMapWithSize(2), hasEntry + ("name", "Android"), hasKey("version") + ) + ) + ) + assertThat( + verifyMsg.context!!["device"] as Map<*, *>, `is`( + allOf( + hasKey("id"), + hasKey("manufacturer"), + hasKey("model"), + hasKey("name"), + hasKey("type"), + hasKey("adTrackingEnabled"), + ) + ) + ) + assertThat( + verifyMsg.context!!["network"] as Map<*, *>, `is`( + allOf( +// hasKey("carrier"), + hasKey("bluetooth"), + hasKey("cellular"), + hasKey("wifi") + ) + ) + ) + + + } + + + @Test + fun testSetAdvertisingId() { + platformInputsPlugin.setAdvertisingId("testAdvertisingId") + val message = TrackMessage.create( + "testEvent", + timestamp = RudderUtils.timeStamp + ) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + whenever(mockChain.proceed(any())).thenAnswer { + it.arguments[0] as TrackMessage + } + val verifyMsg = platformInputsPlugin.intercept(mockChain) + assertThat( + verifyMsg.context!!["device"] as Map<*, *>, hasEntry("advertisingId", "testAdvertisingId") + ) + } + + @Test + fun testChannelIsSetToMessages() { + platformInputsPlugin.setAdvertisingId("testAdvertisingId") + val message = TrackMessage.create( + "testEvent", + timestamp = RudderUtils.timeStamp + ) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + whenever(mockChain.proceed(any())).thenAnswer { + it.arguments[0] as TrackMessage + } + val verifyMsg = platformInputsPlugin.intercept(mockChain) + assertThat( + verifyMsg, hasProperty("channel", `is`("mobile")) + ) + } + + @Test + fun testPutDeviceToken() { + platformInputsPlugin.putDeviceToken("testDeviceToken") + val message = TrackMessage.create( + "testEvent", + timestamp = RudderUtils.timeStamp + ) + val mockChain = mock() + whenever(mockChain.message()).thenReturn(message) + whenever(mockChain.proceed(any())).thenAnswer { + it.arguments[0] as TrackMessage + } + val verifyMsg = platformInputsPlugin.intercept(mockChain) + assertThat( + verifyMsg!!.context!!["device"] as Map<*, *>, hasEntry("token", "testDeviceToken") + ) + } + +} + +class AndroidContextPluginTestApplication : Application() { + companion object { + const val TAG = "AndroidContextPluginTestApplication" + const val PACKAGE_NAME = "com.rudderstack.android.test" + const val NAME = "TestApp" + } + + override fun getPackageManager(): PackageManager { + val pkm = super.getPackageManager() + val newPkm = spy(pkm) + val packageInfo = PackageInfo().apply { + packageName = PACKAGE_NAME + versionName = "1.0" + versionCode = 1 + longVersionCode = 1 + applicationInfo = ApplicationInfo().apply { + packageName = PACKAGE_NAME + targetSdkVersion = 30 + } + } + + whenever(newPkm.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(packageInfo) + whenever(newPkm.getText(eq(PACKAGE_NAME), any(), anyOrNull())).thenReturn(PACKAGE_NAME) + return newPkm + } + + override fun getPackageName(): String { + return PACKAGE_NAME + } +} diff --git a/android/src/test/java/com/rudderstack/android/providers/TestAnalyticsProvider.kt b/android/src/test/java/com/rudderstack/android/providers/TestAnalyticsProvider.kt new file mode 100644 index 000000000..7f9fdcfaf --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/providers/TestAnalyticsProvider.kt @@ -0,0 +1,25 @@ +package com.rudderstack.android.providers + +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.Storage + +fun provideAnalytics( + writeKey: String, + configuration: Configuration, + dataUploadService: DataUploadService? = null, + configDownloadService: ConfigDownloadService? = null, + storage: Storage? = null, + initializationListener: ((success: Boolean, message: String?) -> Unit)? = null, + shutdownHook: (Analytics.() -> Unit)? = null +) = Analytics( + writeKey = writeKey, + configuration = configuration, + storage = storage, + initializationListener = initializationListener, + dataUploadService = dataUploadService, + configDownloadService = configDownloadService, + shutdownHook = shutdownHook +) diff --git a/android/src/test/java/com/rudderstack/android/storage/MessageEntityTest.kt b/android/src/test/java/com/rudderstack/android/storage/MessageEntityTest.kt new file mode 100644 index 000000000..9ab1d2d46 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/storage/MessageEntityTest.kt @@ -0,0 +1,210 @@ +package com.rudderstack.android.storage + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.rudderstack.core.RudderUtils +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.core.models.GroupMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.ScreenMessage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.rudderjsonadapter.JsonAdapter +import junit.framework.TestSuite +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.robolectric.annotation.Config + +abstract class MessageEntityTest { + abstract val jsonAdapter: JsonAdapter + + @Test + fun testGenerateContentValuesForTrack() { + val testMessage = TrackMessage.create( + "testEvent", + RudderUtils.timeStamp, + mapOf("testKey" to "testValue"), + "testAnonymousId", + "testUserId", + mapOf("dest1" to mapOf("key1" to "value1")), + mapOf("dest2" to mapOf("some_key" to "some_value")), + listOf(mapOf("type" to "byomkesh")), + mapOf("k1" to mapOf("k_1_1" to "v1")) + ) + val messageEntity = MessageEntity(testMessage, jsonAdapter) + val contentValues = messageEntity.generateContentValues() + println("serialised: ${contentValues.get(MessageEntity.ColumnNames.message)}") + assertNotNull(contentValues) + val regainedEntity = MessageEntity.create(contentValues.keySet().associateWith { + contentValues.getAsString(it) + }, jsonAdapter) + MatcherAssert.assertThat( + regainedEntity?.message, Matchers.allOf( + Matchers.notNullValue(), + Matchers.instanceOf(TrackMessage::class.java), + Matchers.hasProperty("eventName", Matchers.equalTo("testEvent")), + Matchers.hasProperty("anonymousId", Matchers.equalTo("testAnonymousId")), + Matchers.hasProperty("userId", Matchers.equalTo("testUserId")), + Matchers.hasProperty("properties", Matchers.hasEntry("testKey", "testValue")), + Matchers.hasProperty("messageId", Matchers.equalTo(testMessage.messageId)), + Matchers.hasProperty("context", Matchers.equalTo(testMessage.context)), + Matchers.hasProperty("destinationProps", Matchers.equalTo(testMessage.destinationProps)), + + ) + ) + } + + @Test + fun testGenerateContentValuesForGroup() { + val testMessage = GroupMessage.create( + "testAnonymousId", + "testUserId", + RudderUtils.timeStamp, + mapOf("dest1" to mapOf("key1" to "value1")), + "testGroup", + mapOf("testKey" to "testValue"), + mapOf("key2" to mapOf("some_key" to "some_value")), + listOf(mapOf("type" to "byomkesh")), + mapOf("k1" to mapOf("k_1_1" to "v1")) + ) + val messageEntity = MessageEntity(testMessage, jsonAdapter) + val contentValues = messageEntity.generateContentValues() + println("serialised: ${contentValues.get(MessageEntity.ColumnNames.message)}") + assertNotNull(contentValues) + val regainedEntity = MessageEntity.create(contentValues.keySet().associateWith { + contentValues.getAsString(it) + }, jsonAdapter) + MatcherAssert.assertThat( + regainedEntity?.message, Matchers.allOf( + Matchers.notNullValue(), + Matchers.instanceOf(GroupMessage::class.java), + Matchers.hasProperty("groupId", Matchers.equalTo(testMessage.groupId)), + Matchers.hasProperty("anonymousId", Matchers.equalTo(testMessage.anonymousId)), + Matchers.hasProperty("userId", Matchers.equalTo(testMessage.userId)), + Matchers.hasProperty("traits", Matchers.hasEntry("testKey", "testValue")), + Matchers.hasProperty("messageId", Matchers.equalTo(testMessage.messageId)), + Matchers.hasProperty("context", Matchers.equalTo(testMessage.context)), + Matchers.hasProperty("destinationProps", Matchers.equalTo(testMessage.destinationProps)), + + ) + ) + } + + @Test + fun testGenerateContentValuesForIdentify() { + val testMessage = IdentifyMessage.create( + "testAnonymousId", + "testUserId", + RudderUtils.timeStamp, + mapOf("testKey" to "testValue"), + mapOf("dest1" to mapOf("key1" to "value1")), + mapOf("key2" to mapOf("some_key" to "some_value")), + listOf(mapOf("type" to "byomkesh")), + mapOf("k1" to mapOf("k_1_1" to "v1")), + ) + val messageEntity = MessageEntity(testMessage, jsonAdapter) + val contentValues = messageEntity.generateContentValues() + assertNotNull(contentValues) + val regainedEntity = MessageEntity.create(contentValues.keySet().associateWith { + contentValues.getAsString(it) + }, jsonAdapter) + MatcherAssert.assertThat( + regainedEntity?.message, Matchers.allOf( + Matchers.notNullValue(), + Matchers.instanceOf(IdentifyMessage::class.java), + Matchers.hasProperty("anonymousId", Matchers.equalTo(testMessage.anonymousId)), + Matchers.hasProperty("userId", Matchers.equalTo(testMessage.userId)), + Matchers.hasProperty("properties", Matchers.hasEntry("testKey", "testValue")), + Matchers.hasProperty("messageId", Matchers.equalTo(testMessage.messageId)), + Matchers.hasProperty("context", Matchers.equalTo(testMessage.context)), + Matchers.hasProperty("destinationProps", Matchers.equalTo(testMessage.destinationProps)), + + ) + ) + } + + @Test + fun testGenerateContentValuesForScreen() { + val testMessage = ScreenMessage.create( + name = "testScreen", + RudderUtils.timeStamp, + "testAnonymousId", + "testUserId", + mapOf("dest1" to mapOf("key1" to "value1")), + category = "testCategory", + mapOf("testKey" to "testValue"), + mapOf("key2" to mapOf("some_key" to "some_value")), + listOf(mapOf("type" to "byomkesh")), + mapOf("k1" to mapOf("k_1_1" to "v1")), + ) + val messageEntity = MessageEntity(testMessage, jsonAdapter) + val contentValues = messageEntity.generateContentValues() + assertNotNull(contentValues) + val regainedEntity = MessageEntity.create(contentValues.keySet().associateWith { + contentValues.getAsString(it) + }, jsonAdapter) + MatcherAssert.assertThat( + regainedEntity?.message, Matchers.allOf( + Matchers.notNullValue(), + Matchers.instanceOf(ScreenMessage::class.java), + Matchers.hasProperty("anonymousId", Matchers.equalTo(testMessage.anonymousId)), + Matchers.hasProperty("userId", Matchers.equalTo(testMessage.userId)), + Matchers.hasProperty("properties", Matchers.hasEntry("testKey", "testValue")), + Matchers.hasProperty("messageId", Matchers.equalTo(testMessage.messageId)), + Matchers.hasProperty("context", Matchers.equalTo(testMessage.context)), + Matchers.hasProperty("destinationProps", Matchers.equalTo(testMessage.destinationProps)), + + ) + ) + } + + @Test + fun testGetPrimaryKeyValues() { + val testMessage = TrackMessage.create( + "testEvent", + RudderUtils.timeStamp, + mapOf("testKey" to "testValue"), + "testAnonymousId", + "testUserId", + mapOf("dest1" to mapOf("key1" to "value1")), + mapOf("dest2" to mapOf("some_key" to "some_value")), + listOf(mapOf("type" to "byomkesh")), + mapOf("k1" to mapOf("k_1_1" to "v1")) + ) + val messageEntity = MessageEntity(testMessage, jsonAdapter) + val primaryKeyValues = messageEntity.getPrimaryKeyValues() + MatcherAssert.assertThat(primaryKeyValues, Matchers.arrayContaining(testMessage.messageId)) + } +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class GsonEntityTest : MessageEntityTest() { + override val jsonAdapter: JsonAdapter + get() = GsonAdapter() +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class JacksonEntityTest : MessageEntityTest() { + override val jsonAdapter: JsonAdapter + get() = JacksonAdapter() +} + +/*@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class MoshiEntityTest : MessageEntityTest() { + override val jsonAdapter: JsonAdapter + get() = MoshiAdapter() +}*/ + +@RunWith(Suite::class) +@Suite.SuiteClasses( + GsonEntityTest::class, JacksonEntityTest::class, + //TODO fix moshi adapter +// MoshiEntityTest::class +) +class MessageEntityTestSuite : TestSuite() {} diff --git a/android/src/test/java/com/rudderstack/android/storage/MigrateV1ToV2UtilsTest.kt b/android/src/test/java/com/rudderstack/android/storage/MigrateV1ToV2UtilsTest.kt new file mode 100644 index 000000000..7b93e07c2 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/storage/MigrateV1ToV2UtilsTest.kt @@ -0,0 +1,594 @@ +package com.rudderstack.android.storage + +import android.content.ContentValues +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.SerializedName +import com.rudderstack.android.repository.Entity +import com.rudderstack.android.repository.RudderDatabase +import com.rudderstack.android.repository.annotation.RudderEntity +import com.rudderstack.android.repository.annotation.RudderField +import com.rudderstack.android.utils.TestExecutor +import com.rudderstack.android.utils.busyWait +import com.rudderstack.core.RudderUtils +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.core.models.Message +import com.rudderstack.android.models.RudderApp +import com.rudderstack.android.models.RudderContext +import com.rudderstack.android.models.RudderDeviceInfo +import com.rudderstack.android.models.RudderOSInfo +import com.rudderstack.android.models.RudderScreenInfo +import com.rudderstack.android.models.RudderTraits +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.squareup.moshi.Json +import junit.framework.TestSuite +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.greaterThanOrEqualTo +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.robolectric.annotation.Config +import java.lang.reflect.Type +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +abstract class MigrateV1ToV2UtilsTest { + abstract val jsonAdapter: JsonAdapter + private fun v1Entity1(type: String) = V1Entity( + V1Message( + messageId = "messageId1", + channel = "mobile", + context = RudderContext().also { + it.app = RudderApp( + "build", "name", "namespace", "1.0" + ) + it.customContextMap = mutableMapOf("key_cc" to "value_cc") + it.os = RudderOSInfo( + "name", "version" + ) + it.device = RudderDeviceInfo( + "id", "manufacturer", "model", "name", "type", "token" + ) + it.screen = RudderScreenInfo( + 10, 45, 60 + ) + it.locale = "locale" + }, + type = type, + action = "action", + timestamp = "timestamp", + anonymousId = "anonymousId", + userId = "userId", + event = "event", + properties = mapOf("key1" to "value1"), + userProperties = mapOf("key2" to "value2"), + integrations = mapOf("key3" to true), + destinationProps = mapOf("key4" to mapOf("key5" to "value5")), + previousId = "previousId", + traits = RudderTraits( + RudderTraits.Address( + "city", "country", "7474747", "state", "street" + ), + "email", + "19/12/2005", + RudderTraits.Company().also { + it.putId("id") + it.putName("name") + it.putIndustry("industry") + }, + "19/01/1992", + "description", + "c_name@gmail.com", + "firstName", + "male", + "id", + "lastName", + "my_name", + "023-8393939", + "title", + "username", + ), + + groupId = "groupId" + ) + ) + + private lateinit var v2Database: RudderDatabase + + @Before + fun setUp() { + v2Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence-default.db", + entityFactory = RudderEntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(100) + } + + @After + fun tearDown() { + v2Database.shutDown() + } + + @Test + fun testMigrateTrackV1ToV2() { + + var v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + var v1Dao = v1Database.getDao(V1Entity::class.java) + val v1Entity1 = v1Entity1("track") + println(v1Entity1.v1Message) + with(v1Dao) { + listOf(v1Entity1).insertSync() + } + v1Database.shutDown() + + migrateV1MessagesToV2Database( + ApplicationProvider.getApplicationContext(), + v2Database, + jsonAdapter, + null, + TestExecutor() + ) + //assert that v1 database is empty + v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + v1Dao = v1Database.getDao(V1Entity::class.java) + MatcherAssert.assertThat(v1Dao.getAllSync()?.size, Matchers.equalTo(0)) + v1Database.shutDown() + val v2Dao = v2Database.getDao(MessageEntity::class.java) + val v2Entities = v2Dao.getAllSync() + MatcherAssert.assertThat(v2Entities?.size ?: 0, greaterThanOrEqualTo(1)) + val v2Entity = v2Entities?.get(0) + MatcherAssert.assertThat( + v2Entity?.message, allOf( + Matchers.notNullValue(), + hasProperty("messageId", Matchers.equalTo(v1Entity1.v1Message.messageId)), + hasProperty("channel", Matchers.equalTo(v1Entity1.v1Message.channel)), + hasProperty("eventName", Matchers.equalTo(v1Entity1.v1Message.event)), + hasProperty( + "type", Matchers.equalTo( + Message.EventType.fromValue( + v1Entity1.v1Message.type!! + ) + ) + ), +// hasProperty("action", Matchers.equalTo(v1Entity1.action)), not required + hasProperty("timestamp", Matchers.equalTo(v1Entity1.v1Message.timestamp)), + hasProperty("anonymousId", Matchers.equalTo(v1Entity1.v1Message.anonymousId)), + hasProperty("userId", Matchers.equalTo(v1Entity1.v1Message.userId)), + hasProperty("properties", Matchers.equalTo(v1Entity1.v1Message.properties)), +// hasProperty("userProperties", Matchers.equalTo(v1Entity1.userProperties)), + hasProperty("integrations", Matchers.equalTo(v1Entity1.v1Message.integrations)), + hasProperty( + "destinationProps", Matchers.equalTo(v1Entity1.v1Message.destinationProps) + ), +// hasProperty("previousId", Matchers.equalTo(v1Entity1.v1Message.previousId)), +// hasProperty("groupId", Matchers.equalTo(v1Entity1.v1Message.groupId)) + ) + ) + } + + @Test + fun testMigrateGroupV1ToV2() { + + var v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + var v1Dao = v1Database.getDao(V1Entity::class.java) + val v1Entity1 = v1Entity1("group") + println(v1Entity1.v1Message) + with(v1Dao) { + listOf(v1Entity1).insertSync() + } + v1Database.shutDown() + + migrateV1MessagesToV2Database( + ApplicationProvider.getApplicationContext(), + v2Database, + jsonAdapter, + executorService = TestExecutor() + ) + //assert that v1 database is empty + v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + v1Dao = v1Database.getDao(V1Entity::class.java) + MatcherAssert.assertThat(v1Dao.getAllSync()?.size, Matchers.equalTo(0)) + v1Database.shutDown() + val v2Dao = v2Database.getDao(MessageEntity::class.java) + val v2Entities = v2Dao.getAllSync() + MatcherAssert.assertThat(v2Entities?.size ?: 0, greaterThanOrEqualTo(1)) + val v2Entity = v2Entities?.get(0) + MatcherAssert.assertThat( + v2Entity?.message, allOf( + Matchers.notNullValue(), + hasProperty("messageId", Matchers.equalTo(v1Entity1.v1Message.messageId)), + hasProperty("channel", Matchers.equalTo(v1Entity1.v1Message.channel)), +// hasProperty("eventName", Matchers.equalTo(v1Entity1.v1Message.event)), + hasProperty( + "type", Matchers.equalTo( + Message.EventType.fromValue( + v1Entity1.v1Message.type!! + ) + ) + ), +// hasProperty("action", Matchers.equalTo(v1Entity1.action)), not required + hasProperty("timestamp", Matchers.equalTo(v1Entity1.v1Message.timestamp)), + hasProperty("anonymousId", Matchers.equalTo(v1Entity1.v1Message.anonymousId)), + hasProperty("userId", Matchers.equalTo(v1Entity1.v1Message.userId)), +// hasProperty("properties", Matchers.equalTo(v1Entity1.v1Message.properties)), +// hasProperty("userProperties", Matchers.equalTo(v1Entity1.userProperties)), + hasProperty("integrations", Matchers.equalTo(v1Entity1.v1Message.integrations)), + hasProperty( + "destinationProps", Matchers.equalTo(v1Entity1.v1Message.destinationProps) + ), +// hasProperty("previousId", Matchers.equalTo(v1Entity1.v1Message.previousId)), + hasProperty("groupId", Matchers.equalTo(v1Entity1.v1Message.groupId)) + ) + ) + } + + @Test + fun testMigrateIdentifyV1ToV2() { + + var v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + var v1Dao = v1Database.getDao(V1Entity::class.java) + val v1Entity1 = v1Entity1("identify") + println(v1Entity1.v1Message) + with(v1Dao) { + listOf(v1Entity1).insertSync() + } + v1Database.shutDown() + + migrateV1MessagesToV2Database( + ApplicationProvider.getApplicationContext(), + v2Database, + jsonAdapter, + executorService = TestExecutor() + ) + //assert that v1 database is empty + v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + v1Dao = v1Database.getDao(V1Entity::class.java) + MatcherAssert.assertThat(v1Dao.getAllSync()?.size, Matchers.equalTo(0)) + v1Database.shutDown() + val v2Dao = v2Database.getDao(MessageEntity::class.java) + val v2Entities = v2Dao.getAllSync() + MatcherAssert.assertThat(v2Entities?.size ?: 0, greaterThanOrEqualTo(1)) + val v2Entity = v2Entities?.get(0) + MatcherAssert.assertThat( + v2Entity?.message, allOf( + Matchers.notNullValue(), + hasProperty("messageId", Matchers.equalTo(v1Entity1.v1Message.messageId)), + hasProperty("channel", Matchers.equalTo(v1Entity1.v1Message.channel)), +// hasProperty("eventName", Matchers.equalTo(v1Entity1.v1Message.event)), + hasProperty( + "type", Matchers.equalTo( + Message.EventType.fromValue( + v1Entity1.v1Message.type!! + ) + ) + ), +// hasProperty("action", Matchers.equalTo(v1Entity1.action)), not required + hasProperty("timestamp", Matchers.equalTo(v1Entity1.v1Message.timestamp)), + hasProperty("anonymousId", Matchers.equalTo(v1Entity1.v1Message.anonymousId)), + hasProperty("userId", Matchers.equalTo(v1Entity1.v1Message.userId)), + hasProperty("properties", Matchers.equalTo(v1Entity1.v1Message.properties)), +// hasProperty("userProperties", Matchers.equalTo(v1Entity1.userProperties)), + hasProperty("integrations", Matchers.equalTo(v1Entity1.v1Message.integrations)), + hasProperty( + "destinationProps", Matchers.equalTo(v1Entity1.v1Message.destinationProps) + ), +// hasProperty("previousId", Matchers.equalTo(v1Entity1.v1Message.previousId)), +// hasProperty("groupId", Matchers.equalTo(v1Entity1.v1Message.groupId)) + ) + ) + } + + @Test + fun testMigrateScreenV1ToV2() { + + var v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + var v1Dao = v1Database.getDao(V1Entity::class.java) + val v1Entity1 = v1Entity1("screen") + println(v1Entity1.v1Message) + with(v1Dao) { + listOf(v1Entity1).insertSync() + } + v1Database.shutDown() + + migrateV1MessagesToV2Database( + ApplicationProvider.getApplicationContext(), + v2Database, + jsonAdapter, + executorService = TestExecutor() + ) + //assert that v1 database is empty + v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + v1Dao = v1Database.getDao(V1Entity::class.java) + MatcherAssert.assertThat(v1Dao.getAllSync()?.size, Matchers.equalTo(0)) + v1Database.shutDown() + val v2Dao = v2Database.getDao(MessageEntity::class.java) + val v2Entities = v2Dao.getAllSync() + MatcherAssert.assertThat(v2Entities?.size ?: 0, greaterThanOrEqualTo(1)) + val v2Entity = v2Entities?.get(0) + MatcherAssert.assertThat( + v2Entity?.message, allOf( + Matchers.notNullValue(), + hasProperty("messageId", Matchers.equalTo(v1Entity1.v1Message.messageId)), + hasProperty("channel", Matchers.equalTo(v1Entity1.v1Message.channel)), + hasProperty("eventName", Matchers.equalTo(v1Entity1.v1Message.event)), + hasProperty( + "type", Matchers.equalTo( + Message.EventType.fromValue( + v1Entity1.v1Message.type!! + ) + ) + ), + hasProperty("timestamp", Matchers.equalTo(v1Entity1.v1Message.timestamp)), + hasProperty("anonymousId", Matchers.equalTo(v1Entity1.v1Message.anonymousId)), + hasProperty("userId", Matchers.equalTo(v1Entity1.v1Message.userId)), + hasProperty("properties", Matchers.equalTo(v1Entity1.v1Message.properties)), +// hasProperty("userProperties", Matchers.equalTo(v1Entity1.userProperties)), + hasProperty("integrations", Matchers.equalTo(v1Entity1.v1Message.integrations)), + hasProperty( + "destinationProps", Matchers.equalTo(v1Entity1.v1Message.destinationProps) + ), +// hasProperty("previousId", Matchers.equalTo(v1Entity1.v1Message.previousId)), +// hasProperty("groupId", Matchers.equalTo(v1Entity1.v1Message.groupId)) + ) + ) + } + + @Test + fun testCloudModeEventsFilteredWhenCloudModeDoneV1ToV2() { + + var v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + var v1Dao = v1Database.getDao(V1Entity::class.java) + val v1Entity1 = v1Entity1("screen") + v1Entity1.status = V1_STATUS_CLOUD_MODE_DONE + with(v1Dao) { + listOf(v1Entity1).insertSync() + } + v1Database.shutDown() + + migrateV1MessagesToV2Database( + ApplicationProvider.getApplicationContext(), + v2Database, + jsonAdapter, + executorService = TestExecutor() + ) + //assert that v1 database is empty + v1Database = RudderDatabase( + ApplicationProvider.getApplicationContext(), + databaseName = "rl_persistence.db", + entityFactory = V1EntityFactory(jsonAdapter), + providedExecutorService = TestExecutor() + ) + busyWait(50) + v1Dao = v1Database.getDao(V1Entity::class.java) + MatcherAssert.assertThat(v1Dao.getAllSync()?.size, Matchers.equalTo(0)) + v1Database.shutDown() + val v2Dao = v2Database.getDao(MessageEntity::class.java) + val v2Entities = v2Dao.getAllSync() + MatcherAssert.assertThat(v2Entities?.size ?: -1, `is`(0)) + } + + @RudderEntity( + tableName = "events", fields = arrayOf( + RudderField( + RudderField.Type.TEXT, V1Entity.ColumnNames.MESSAGE_ID_COL, primaryKey = true + ), RudderField(RudderField.Type.TEXT, V1Entity.ColumnNames.MESSAGE_COL), RudderField( + RudderField.Type.INTEGER, V1Entity.ColumnNames.UPDATED_COL, isIndex = true + ), RudderField( + RudderField.Type.INTEGER, V1Entity.ColumnNames.STATUS_COL + ), RudderField( + RudderField.Type.INTEGER, V1Entity.ColumnNames.DM_PROCESSED_COL + ) + ) + ) + class V1Entity( + val v1Message: V1Message + ) : Entity { + val BACKSLASH = "\\\\'" + + object ColumnNames { + const val MESSAGE_COL = "message" + const val UPDATED_COL: String = "updated" + const val MESSAGE_ID_COL: String = "id" + const val STATUS_COL: String = "status" + const val DM_PROCESSED_COL = "dm_processed" + } + + var status = V1_STATUS_NEW + + private val gsonAdapter = GsonAdapter( + GsonBuilder().registerTypeAdapter(RudderTraits::class.java, RudderTraitsTypeAdapter()) + .registerTypeAdapter(RudderContext::class.java, RudderContextTypeAdapter()).create() + ) + + override fun generateContentValues(): ContentValues { + val messageJson = gsonAdapter.writeToJson(v1Message)?.replace("'", BACKSLASH) + return ContentValues().also { + it.put(ColumnNames.MESSAGE_ID_COL, v1Message.messageId) + it.put( + ColumnNames.MESSAGE_COL, + messageJson, + ) + it.put(ColumnNames.UPDATED_COL, System.currentTimeMillis()) + it.put(ColumnNames.STATUS_COL, status) + it.put(ColumnNames.DM_PROCESSED_COL, 1) + } + } + + override fun getPrimaryKeyValues(): Array { + return arrayOf(v1Message.messageId!!) + } + } +} + +data class V1Message( + val messageId: String? = UUID.randomUUID().toString(), + val channel: String? = "mobile", + val context: RudderContext? = null, + val type: String? = null, + val action: String? = null, + @SerializedName("originalTimestamp") @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") val timestamp: String? = RudderUtils.timeStamp, + val anonymousId: String? = null, + val userId: String? = null, + val event: String? = null, + val properties: Map? = null, + val userProperties: Map? = null, + val integrations: Map? = HashMap(), + val destinationProps: Map?>? = null, + val previousId: String? = null, + val traits: RudderTraits? = null, + val groupId: String? = null +) + +class RudderContextTypeAdapter : JsonSerializer { + override fun serialize( + rudderContext: RudderContext?, typeOfSrc: Type, context: JsonSerializationContext + ): JsonElement? { + return try { + val outputContext = JsonObject() + val gson = Gson() + val inputContext = gson.toJsonTree(rudderContext) as JsonObject + for ((key, value) in inputContext.entrySet()) { + if (key == "customContextMap") { + val customContextMapObject = gson.toJsonTree(value) as JsonObject + for ((key1, value1) in customContextMapObject.entrySet()) { + outputContext.add(key1, value1) + } + continue + } + outputContext.add(key, value) + } + outputContext + } catch (e: Exception) { + e.toString() + null + } + } +} + +class RudderTraitsTypeAdapter : JsonSerializer { + override fun serialize( + traits: RudderTraits?, typeOfSrc: Type, context: JsonSerializationContext + ): JsonElement? { + return try { + val outputTraits = JsonObject() + val gson = Gson() + val inputTraits = gson.toJsonTree(traits) as JsonObject + for ((key, value) in inputTraits.entrySet()) { + if (key == "extras") { + val extrasObject = gson.toJsonTree(value) as JsonObject + for ((key1, value1) in extrasObject.entrySet()) { + outputTraits.add(key1, value1) + } + continue + } + outputTraits.add(key, value) + } + outputTraits + } catch (e: java.lang.Exception) { + null + } + } + + +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class GsonMigrateV1ToV2UtilsTest : MigrateV1ToV2UtilsTest() { + override val jsonAdapter: JsonAdapter + get() = GsonAdapter() +} + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class JacksonMigrateV1ToV2UtilsTest : MigrateV1ToV2UtilsTest() { + override val jsonAdapter: JsonAdapter + get() = JacksonAdapter() +} + +/*@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class MoshiMigrateV1ToV2UtilsTest : MigrateV1ToV2UtilsTest() { + override val jsonAdapter: JsonAdapter + get() = MoshiAdapter() +}*/ + +@RunWith(Suite::class) +@Suite.SuiteClasses( + GsonMigrateV1ToV2UtilsTest::class, JacksonMigrateV1ToV2UtilsTest::class, + //TODO fix moshi adapter +// MoshiMigrateV1ToV2UtilsTest::class +) +class MigrateV1ToV2UtilsTestSuite : TestSuite() {} + diff --git a/android/src/test/java/com/rudderstack/android/utilities/AnalyticsUtilTest.kt b/android/src/test/java/com/rudderstack/android/utilities/AnalyticsUtilTest.kt new file mode 100644 index 000000000..9c4d08337 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/utilities/AnalyticsUtilTest.kt @@ -0,0 +1,46 @@ +package com.rudderstack.android.utilities + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.AnalyticsRegistry +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.RudderAnalytics.Companion.getInstance +import com.rudderstack.core.Logger +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class AnalyticsUtilTest { + + @Before + fun setUp() { + AnalyticsRegistry.clear() + } + + @Test + fun `given writeKey and configuration are passed, when anonymousId id is set, then assert that configuration has this anonymousId set as a property`() { + val analytics = getInstance( + "testKey", ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + logLevel = Logger.LogLevel.DEBUG, + ) + ) + + analytics.setAnonymousId("anon_id") + MatcherAssert.assertThat( + analytics.currentConfigurationAndroid, allOf( + Matchers.isA(ConfigurationAndroid::class.java), + Matchers.hasProperty("anonymousId", Matchers.equalTo("anon_id")) + ) + ) + } +} diff --git a/android/src/test/java/com/rudderstack/android/utilities/SessionUtilsTest.kt b/android/src/test/java/com/rudderstack/android/utilities/SessionUtilsTest.kt new file mode 100644 index 000000000..ecdb79278 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/utilities/SessionUtilsTest.kt @@ -0,0 +1,403 @@ +package com.rudderstack.android.utilities + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.internal.states.UserSessionState +import com.rudderstack.android.models.UserSession +import com.rudderstack.android.storage.AndroidStorage +import com.rudderstack.core.Analytics +import com.rudderstack.core.Logger +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.holder.removeState +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.not +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class SessionUtilsTest { + private lateinit var analytics: Analytics + private lateinit var mockStorage: AndroidStorage + private val userSessionState: UserSessionState? + get() = analytics.retrieveState() + + @Before + fun setup() { + mockStorage = mock() + analytics = generateTestAnalytics( + mockConfiguration = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ), + storage = mockStorage + ) + analytics.associateState(UserSessionState()) + } + + @After + fun teardown() { + analytics.removeState() + analytics.storage.clearStorage() + analytics.shutdown() + } + + @Test + fun `test start session`() { + analytics.startSession() + val session = userSessionState?.value + assertNotNull(session) + assertTrue(session?.isActive == true) + assertTrue(session?.sessionId != null) + assertTrue(session?.sessionStart == true) + assertTrue(session?.lastActiveTimestamp != null) + } + + @Test + fun `test start session with valid sessionId`() { + val sessionId = 1234567890L + analytics.startSession(sessionId) + MatcherAssert.assertThat( + (analytics.currentConfigurationAndroid)?.trackAutoSession, + `is`(false) + ) + val session = userSessionState?.value + assertNotNull(session) + assertTrue(session?.isActive == true) + MatcherAssert.assertThat(session?.sessionId, `is`(sessionId)) + assertTrue(session?.sessionStart == true) + assertTrue(session?.lastActiveTimestamp != null) + } + + @Test + fun `test startSession with invalid sessionId`() { + // Given + val invalidSessionId = 123456789L + + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.startSession(invalidSessionId) + + //verify SessionState is not updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(-1L)) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(-1L)) + MatcherAssert.assertThat(session?.isActive, `is`(false)) + MatcherAssert.assertThat(session?.sessionStart, `is`(false)) + } + + @Test + fun `test endSession`() { + analytics.startSession() + + val session = userSessionState?.value + //check session is started + MatcherAssert.assertThat(session?.sessionId, `is`(not(-1L))) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(not(-1L))) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + MatcherAssert.assertThat(session?.sessionStart, `is`(true)) + // When + analytics.endSession() + // verify + val sessionAfterEnd = userSessionState?.value + MatcherAssert.assertThat(sessionAfterEnd?.sessionId, `is`(-1L)) + MatcherAssert.assertThat(sessionAfterEnd?.lastActiveTimestamp, `is`(-1L)) + MatcherAssert.assertThat(sessionAfterEnd?.isActive, `is`(false)) + MatcherAssert.assertThat(sessionAfterEnd?.sessionStart, `is`(false)) + //should update trackAutoSession to false + MatcherAssert.assertThat( + analytics.currentConfigurationAndroid?.trackAutoSession, + `is`(false) + ) + + } + + @Test + fun `test startSessionIfNeeded with a new session`() { + // Given + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = JacksonAdapter(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + userSessionState?.update(UserSession()) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.startSessionIfNeeded() + + // Then + // Verify that SessionState is updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(not(-1L))) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(not(-1L))) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + MatcherAssert.assertThat(session?.sessionStart, `is`(true)) + } + + @Test + fun `test startSessionIfNeeded not updating session when session is ongoing`() { + // Given + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + sessionTimeoutMillis = 10000L, + logLevel = Logger.LogLevel.DEBUG, + ) + val sessionId = 1234567890L + val lastActiveTimestamp = defaultLastActiveTimestamp + userSessionState?.update( + UserSession( + sessionId = sessionId, isActive = true, lastActiveTimestamp = lastActiveTimestamp + ) + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.startSessionIfNeeded() + + // Then + // Verify that SessionState is not updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(sessionId)) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(lastActiveTimestamp)) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + } + + @Test + fun `test startSessionIfNeeded with an expired session`() { +// Given + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + val sessionId = 1234567890L + val lastActiveTimestamp = System.currentTimeMillis() - mockConfig.sessionTimeoutMillis + userSessionState?.update( + UserSession( + sessionId = sessionId, isActive = true, lastActiveTimestamp = lastActiveTimestamp + ) + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.startSessionIfNeeded() + + // Then + // Verify that SessionState is updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(not(sessionId))) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(not(lastActiveTimestamp))) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + MatcherAssert.assertThat(session?.sessionStart, `is`(true)) + } + + @Test + fun `test startSessionIfNeeded with an expired session and sessionTimeoutMillis is 0`() { + // Given + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + sessionTimeoutMillis = 0L, + logLevel = Logger.LogLevel.DEBUG, + ) + val sessionId = 1234567890L + val lastActiveTimestamp = System.currentTimeMillis() - mockConfig.sessionTimeoutMillis + userSessionState?.update( + UserSession( + sessionId = sessionId, isActive = true, lastActiveTimestamp = lastActiveTimestamp + ) + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.startSessionIfNeeded() + + // Then + // Verify that SessionState is updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(not(sessionId))) +// MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(not(lastActiveTimestamp))) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + MatcherAssert.assertThat(session?.sessionStart, `is`(true)) + } + + @Test + fun `test initializeSessionManagement with a saved ongoing session`() { + // Given + val sessionId = 1234567890L + val lastActiveTimestamp = defaultLastActiveTimestamp + whenever(mockStorage.sessionId).thenReturn(sessionId) + whenever(mockStorage.lastActiveTimestamp).thenReturn(lastActiveTimestamp) + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.initializeSessionManagement(mockStorage.sessionId, mockStorage.lastActiveTimestamp) + + // Then + // Verify that SessionState is not updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, `is`(sessionId)) + MatcherAssert.assertThat(session?.lastActiveTimestamp, `is`(lastActiveTimestamp)) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + } + + @Test + fun `test initializeSessionManagement with a saved but expired session`() { + // Given + val sessionId = 1234567890L + val lastActiveTimestamp = + defaultLastActiveTimestamp - ConfigurationAndroid.SESSION_TIMEOUT - 1L //expired session + whenever(mockStorage.sessionId).thenReturn(sessionId) + whenever(mockStorage.lastActiveTimestamp).thenReturn(lastActiveTimestamp) + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.initializeSessionManagement(null, null) + + // Then + // Verify that SessionState is updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, allOf(not(sessionId), not(-1L))) + MatcherAssert.assertThat( + session?.lastActiveTimestamp, allOf( + not(lastActiveTimestamp), not(-1L) + ) + ) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + } + + @Test + fun `test initializeSessionManagement without saved session`() { + // Given + whenever(mockStorage.sessionId).thenReturn(null) + whenever(mockStorage.lastActiveTimestamp).thenReturn(null) + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics.applyConfiguration { + mockConfig + } + // When + analytics.initializeSessionManagement(null, null) + // Verify that SessionState is updated + val session = userSessionState?.value + MatcherAssert.assertThat(session?.sessionId, not(-1L)) + MatcherAssert.assertThat(session?.lastActiveTimestamp, not(-1L)) + MatcherAssert.assertThat(session?.isActive, `is`(true)) + } + + @Test + fun `test listenToSessionChanges with active session`() { +// Given + whenever(mockStorage.sessionId).thenReturn(null) + whenever(mockStorage.lastActiveTimestamp).thenReturn(null) + val mockConfig = ConfigurationAndroid( + application = ApplicationProvider.getApplicationContext(), + jsonAdapter = mock(), + shouldVerifySdk = false, + trackLifecycleEvents = true, + trackAutoSession = true, + logLevel = Logger.LogLevel.DEBUG, + ) + analytics.shutdown() + analytics = generateTestAnalytics( + mockConfiguration = mockConfig, + storage = mockStorage + ) + // When + analytics.initializeSessionManagement() + val sessionId = 1234567890L + val lastActiveTimestamp = defaultLastActiveTimestamp + userSessionState?.update( + UserSession( + lastActiveTimestamp, sessionId, isActive = true, false + ) + ) + // Verify that storage is updated + val sessionIdCapturer = argumentCaptor() + verify(mockStorage, atLeast(2)).setSessionId(sessionIdCapturer.capture()) // once on + // session start, one for session update + MatcherAssert.assertThat(sessionIdCapturer.allValues, hasItem(sessionId)) + val lastActiveTimestampCapturer = argumentCaptor() + verify(mockStorage, atLeast(2)).saveLastActiveTimestamp( + lastActiveTimestampCapturer.capture() + ) // + // once on + // session start, one for session update + MatcherAssert.assertThat(lastActiveTimestampCapturer.allValues, hasItem(lastActiveTimestamp)) + } + +} diff --git a/android/src/test/java/com/rudderstack/android/utilities/V1MigratoryUtilsKtTest.kt b/android/src/test/java/com/rudderstack/android/utilities/V1MigratoryUtilsKtTest.kt new file mode 100644 index 000000000..9c4df5be2 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/utilities/V1MigratoryUtilsKtTest.kt @@ -0,0 +1,45 @@ +package com.rudderstack.android.utilities + +import android.app.Application +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.rudderstack.android.storage.saveObject +import com.rudderstack.core.internal.KotlinLogger +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +class V1MigratoryUtilsKtTest { + private val context = ApplicationProvider.getApplicationContext() + + @Test + fun `test sourceId should not exist`() { + val isSourceIdExist = context.isV1SavedServerConfigContainsSourceId("fileName", "new_source_id") + assertThat(isSourceIdExist, Matchers.`is`(false)) + } + + @Test + fun `test wrong sourceId exists`() { + val fileName = "file_name" + //create a file + saveObject("dummy", context, fileName, KotlinLogger()) + val isSourceIdExist = context.isV1SavedServerConfigContainsSourceId(fileName, "new_source_id") + assertThat(isSourceIdExist, Matchers.`is`(false)) + } + + @Test + fun `test sourceId should exist`() { + + val sourceId = "i_am_source_id" + val fileName = "file_name" + //create a file + saveObject("my source id is $sourceId", context, fileName, KotlinLogger()) + val isSourceIdExist = context.isV1SavedServerConfigContainsSourceId(fileName, sourceId) + assertThat(isSourceIdExist, Matchers.`is`(true)) + } +} diff --git a/android/src/test/java/com/rudderstack/android/utils/TestExecutor.kt b/android/src/test/java/com/rudderstack/android/utils/TestExecutor.kt new file mode 100644 index 000000000..75129aaf3 --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/utils/TestExecutor.kt @@ -0,0 +1,48 @@ +/* + * Creator: Debanjan Chatterjee on 16/06/23, 8:58 pm Last modified: 16/06/23, 8:58 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.utils + +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.TimeUnit + +class TestExecutor : AbstractExecutorService() { + private var _isShutdown = false + override fun execute(command: Runnable?) { +command?.run() + } + + override fun shutdown() { + //No op + _isShutdown = true + } + + override fun shutdownNow(): MutableList { + // No op + shutdown() + return mutableListOf() + } + + override fun isShutdown(): Boolean { + return _isShutdown + } + + override fun isTerminated(): Boolean { + return _isShutdown + } + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { + return false + } +} \ No newline at end of file diff --git a/android/src/test/java/com/rudderstack/android/utils/Utils.kt b/android/src/test/java/com/rudderstack/android/utils/Utils.kt new file mode 100644 index 000000000..4d3df620b --- /dev/null +++ b/android/src/test/java/com/rudderstack/android/utils/Utils.kt @@ -0,0 +1,22 @@ +/* + * Creator: Debanjan Chatterjee on 27/12/23, 11:41 am Last modified: 27/12/23, 11:41 am + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.utils + +fun busyWait(millis: Long) { + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < millis) { + // busy wait + } +} \ No newline at end of file diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md deleted file mode 100644 index f7f3a262e..000000000 --- a/app/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -### 1.0.1 (2023-07-13) - - -### Bug Fixes - -* adding gradle files and github actions ([df7d7f2](https://github.com/rudderlabs/rudder-sdk-android/commit/df7d7f2fef54c2f1ac8435c576a66429aa10ab3f)) -* gradle files ([1522e25](https://github.com/rudderlabs/rudder-sdk-android/commit/1522e25197749caee93c7814e3c1836e7acb2b7b)) diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 2ac94d894..000000000 --- a/app/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'com.android.application' - id 'kotlin-android' -} - -android { - compileSdk 31 - namespace 'com.rudderstack.android.demo' - - defaultConfig { - applicationId "com.rudderstack.android.demo" - minSdk 21 - targetSdk 31 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - kotlinOptions { - jvmTarget = '17' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' - testImplementation 'junit:junit:4.+' -// androidTestImplementation 'androidx.test.ext:junit:1.1.2' -// androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' -} \ No newline at end of file diff --git a/app/gradle.properties b/app/gradle.properties deleted file mode 100644 index f2fe3673e..000000000 --- a/app/gradle.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Creator: Debanjan Chatterjee on 13/07/23, 10:14 am Last modified: 13/07/23, 10:14 am -# Copyright: All rights reserved ? 2023 http://rudderstack.com -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain a -# copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -android.useAndroidX=true -android.enableJetifier=true - diff --git a/app/src/androidTest/java/com/rudderstack/android/libs/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/rudderstack/android/libs/ExampleInstrumentedTest.kt deleted file mode 100644 index 75a10cf47..000000000 --- a/app/src/androidTest/java/com/rudderstack/android/libs/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.rudderstack.android.libs - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rudderstack.android.libs", appContext.packageName) - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b61a7804d..000000000 --- a/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath "com.android.tools.build:gradle:7.4.2" - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} -plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.3.0" -} - -task clean(type: Delete) { - delete rootProject.buildDir -} -apply from: rootProject.file('gradle/promote.gradle') -apply from: rootProject.file('gradle/codecov.gradle') -subprojects { - group GROUP - version getVersionName(VERSION_NAME) -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..a8382c80a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + extra["compose_version"] = RudderstackBuildConfig.Kotlin.COMPILER_EXTENSION_VERSION + repositories { + google() + mavenCentral() + } + dependencies { + classpath(libs.gradle) + classpath(libs.kotlin.gradle.plugin) + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle.kts files + } +} + +plugins { + alias(libs.plugins.gradle.nexus.publish) + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.jvm) apply false +} + +subprojects { + version = properties[RudderstackBuildConfig.ReleaseInfo.VERSION_NAME].toString() + group = properties[RudderstackBuildConfig.ReleaseInfo.GROUP_NAME].toString() +} + +apply(from = rootProject.file("gradle/promote.gradle")) +apply(from = rootProject.file("gradle/codecov.gradle")) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..18326b842 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` + id("java-library") +} +repositories { + gradlePluginPortal() +} diff --git a/buildSrc/src/main/kotlin/RudderstackBuildConfig.kt b/buildSrc/src/main/kotlin/RudderstackBuildConfig.kt new file mode 100644 index 000000000..9c206a753 --- /dev/null +++ b/buildSrc/src/main/kotlin/RudderstackBuildConfig.kt @@ -0,0 +1,28 @@ +import org.gradle.api.JavaVersion + +object RudderstackBuildConfig { + + object Build { + val JAVA_VERSION = JavaVersion.VERSION_17 + val JVM_TARGET = "17" + } + + object Android { + val COMPILE_SDK = 34 + val MIN_SDK = 19 + val TARGET_SDK = COMPILE_SDK + } + + object Version { + val VERSION_NAME = "\"2.0\"" + } + + object Kotlin { + val COMPILER_EXTENSION_VERSION = "1.4.8" + } + + object ReleaseInfo { + val VERSION_NAME = "" + val GROUP_NAME = "" + } +} diff --git a/models/.gitignore b/core/.gitignore similarity index 100% rename from models/.gitignore rename to core/.gitignore diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 000000000..04af488ae --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("java-library") + id("kotlin") +} + +dependencies { + api(project(":rudderjsonadapter")) + api(project(":web")) + + compileOnly(libs.gson) + compileOnly(libs.jackson.core) + compileOnly(libs.jackson.module) + compileOnly(libs.moshi) + compileOnly(libs.moshi.kotlin) + compileOnly(libs.moshi.adapters) + + + testImplementation(libs.awaitility) + testImplementation(libs.junit) + testImplementation(libs.hamcrest) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockk) + testImplementation(libs.json.assert) + + testImplementation(project(":moshirudderadapter")) + testImplementation(project(":libs:test-common")) + testImplementation(project(":gsonrudderadapter")) + testImplementation(project(":jacksonrudderadapter")) +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-jar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/core/config.properties b/core/config.properties new file mode 100644 index 000000000..55b549b9e --- /dev/null +++ b/core/config.properties @@ -0,0 +1,4 @@ +rudderCoreSdkVersion="2.0.0-alpha" +libraryName="rudder-kotlin" +platform="java-kotlin" +os_version="1.8" \ No newline at end of file diff --git a/core/gradle.properties b/core/gradle.properties index 7e28fd672..3749ea95c 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -1,4 +1,5 @@ POM_ARTIFACT_ID=core +GROUP=com.rudderstack.kotlin.sdk POM_PACKAGING=jar VERSION_CODE=1 VERSION_NAME=2.0.0 diff --git a/core/project.json b/core/project.json index f92f6f816..077aafe3d 100644 --- a/core/project.json +++ b/core/project.json @@ -6,6 +6,17 @@ "targets": { "build": { "executor": "@jnxplus/nx-gradle:build", + "dependsOn": [ + { + "projects": [ + "gsonrudderadapter", + "jacksonrudderadapter", + "moshirudderadapter", + "web" + ], + "target": "build" + } + ], "outputs": [ "{projectRoot}/core/build" ] @@ -33,7 +44,7 @@ "options": { "baseBranch": "master", "preset": "conventional", - "tagPrefix": "${projectName}@" + "tagPrefix": "{projectName}@" } }, "sync-bumped-version-properties": { @@ -55,7 +66,7 @@ ], "executor": "nx:run-commands", "options": { - "command": "echo 'core-release' && ./gradlew :core:publishToSonatype -Prelease closeAndReleaseSonatypeStagingRepository" + "command": "echo 'core-release' && ./gradlew publishToSonatype -p core -Prelease && ./gradlew findSonatypeStagingRepository closeSonatypeStagingRepository && ./gradlew findSonatypeStagingRepository releaseSonatypeStagingRepository" } }, "snapshot-release": { @@ -64,7 +75,7 @@ ], "executor": "nx:run-commands", "options": { - "command": "echo 'core-snapshot' && ./gradlew :core:publishToSonatype" + "command": "echo 'core-snapshot' && cd core && sh ../gradlew generatePomFileForReleasePublication && ../gradlew publishToSonatype && cd .." } } }, diff --git a/core/src/main/java/com/rudderstack/core/Analytics.kt b/core/src/main/java/com/rudderstack/core/Analytics.kt new file mode 100644 index 000000000..a07c1af14 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Analytics.kt @@ -0,0 +1,271 @@ +package com.rudderstack.core + +import com.rudderstack.core.internal.AnalyticsDelegate +import com.rudderstack.core.internal.ConfigDownloadServiceImpl +import com.rudderstack.core.internal.DataUploadServiceImpl +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.GroupMessage +import com.rudderstack.core.models.GroupTraits +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.IdentifyProperties +import com.rudderstack.core.models.IdentifyTraits +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.models.MessageDestinationProps +import com.rudderstack.core.models.ScreenMessage +import com.rudderstack.core.models.ScreenProperties +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.TrackProperties + +class Analytics private constructor( + private val _delegate: AnalyticsDelegate, +) : Controller by _delegate { + /** + * Contains methods for sending messages over to device mode and cloud mode destinations. + * Developers are required to maintain the object throughout application lifetime, + * since this implementation doesn't directly provide a Singleton Instance. + * + * If this SDK is intended to be used in Android, refrain from using this class directly. + * Also in case of using this class directly, developer has to add their own context plugin, + * to add context to messages. + * synchronicity of events. Other executors are practically usable iff the difference between two events + * is guaranteed to be at least a few ms. + * + * Only [MessageContext] passed in [IdentifyMessage] are stored in [Storage] + */ + constructor( + writeKey: String, + configuration: Configuration, + dataUploadService: DataUploadService? = null, + configDownloadService: ConfigDownloadService? = null, + storage: Storage? = null, + //optional + initializationListener: ((success: Boolean, message: String?) -> Unit)? = null, + //optional called if shutdown is called + shutdownHook: (Analytics.() -> Unit)? = null + ) : this( + _delegate = AnalyticsDelegate( + configuration, storage ?: BasicStorageImpl(), writeKey, dataUploadService ?: DataUploadServiceImpl( + writeKey + ), configDownloadService ?: ConfigDownloadServiceImpl( + writeKey + ), initializationListener, shutdownHook + + ) + ) + + + companion object { + // default base url or rudder-backend-server + internal const val DATA_PLANE_URL = "https://hosted.rudderlabs.com" + + // config-plane url to get the config for the writeKey + internal const val CONTROL_PLANE_URL = "https://api.rudderlabs.com/" + + } + + init { + _delegate.startup(this) + } + + /** + * Track with a built [TrackMessage] + * Date format should be yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + * + * @param message + * @param options + */ + fun track(message: TrackMessage, options: RudderOption? = null) { + processMessage(message, options) + } + + @JvmOverloads + fun track( + eventName: String, + options: RudderOption? = null, + userId: String? = null, + anonymousId: String? = null, + trackProperties: TrackProperties? = null, + traits: Map? = null, + destinationProps: MessageDestinationProps? = null, + ) { + track( + TrackMessage.create( + anonymousId = anonymousId, + traits = traits, + destinationProps = destinationProps, + timestamp = RudderUtils.timeStamp, + eventName = eventName, + properties = trackProperties, + userId = userId + ), options + ) + + } + + /** + * DSL format for track call + * + * analytics.track { + * event { +"event" } + * //or event("event") + * trackProperties { + * //use any of these + * +("property1" to "value1") + * +mapOf("property2" to "value2") + * add("property3" to "value3") + * add(mapOf("property4" to "value4")) + * } + * userId("user_id") + * rudderOptions { + * customContexts { + * +("cc1" to "cp1") + * +("cc2" to "cp2") + * } + * externalIds { + * +(mapOf("ext-1" to "ex1")) + * +(mapOf("ext-2" to "ex2")) + * +listOf(mapOf("ext-3" to "ex3")) + * } + * integrations { + * +("firebase" to true) + * +("amplitude" to false) + * } + * } + * } + * @param scope + */ + fun track(scope: TrackScope.() -> Unit) { + val trackScope = TrackScope() + trackScope.scope() + track(trackScope.message, trackScope.options) + } + + fun screen(message: ScreenMessage, options: RudderOption? = null) { + processMessage(message, options) + } + + @JvmOverloads + fun screen( + screenName: String, + category: String? = null, + options: RudderOption? = null, + screenProperties: ScreenProperties, + anonymousId: String? = null, + userId: String? = null, + destinationProps: MessageDestinationProps? = null, + traits: Map? = null, + ) { + screen( + ScreenMessage.create( + userId = userId, + anonymousId = anonymousId, + destinationProps = destinationProps, + traits = traits, + timestamp = RudderUtils.timeStamp, + category = category, + name = screenName, + properties = screenProperties + ), options + ) + } + + fun screen(scope: ScreenScope.() -> Unit) { + val screenScope = ScreenScope() + screenScope.scope() + screen(screenScope.message, screenScope.options) + } + + fun identify(message: IdentifyMessage, options: RudderOption? = null) { + processMessage(message, options) + } + + @JvmOverloads + fun identify( + userId: String, traits: IdentifyTraits? = null, + anonymousId: String? = null, + options: RudderOption? = null, + properties: IdentifyProperties? = null, + destinationProps: MessageDestinationProps? = null, + ) { + val completeTraits = mapOf("userId" to userId) optAdd traits + identify( + IdentifyMessage.create( + userId = userId, + anonymousId = anonymousId, + destinationProps = destinationProps, + timestamp = RudderUtils.timeStamp, + traits = completeTraits, + properties = properties, + ), options + ) + } + + fun identify(scope: IdentifyScope.() -> Unit) { + val identifyScope = IdentifyScope() + identifyScope.scope() + identify(identifyScope.message, identifyScope.options) + } + + fun alias(message: AliasMessage, options: RudderOption? = null) { + processMessage(message, options) + } + + @JvmOverloads + fun alias( + newId: String, + anonymousId: String? = null, + options: RudderOption? = null, + destinationProps: MessageDestinationProps? = null, + previousId: String? = null, + ) { + val completeTraits = mapOf("userId" to newId) + alias( + AliasMessage.create( + timestamp = RudderUtils.timeStamp, + anonymousId = anonymousId, + previousId = previousId, + destinationProps = destinationProps, + userId = newId, traits = completeTraits + ), + options + ) + } + + fun alias(scope: AliasScope.() -> Unit) { + val aliasScope = AliasScope() + aliasScope.scope() + alias(aliasScope.message, aliasScope.options) + } + + fun group(message: GroupMessage, options: RudderOption? = null) { + processMessage(message, options) + } + + @JvmOverloads + fun group( + groupId: String?, + options: RudderOption? = null, + userId: String? = null, + anonymousId: String? = null, + groupTraits: GroupTraits?, + destinationProps: MessageDestinationProps? = null, + ) { + group( + GroupMessage.create( + timestamp = RudderUtils.timeStamp, + userId = userId, + groupId = groupId, + groupTraits = groupTraits, + anonymousId = anonymousId, + destinationProps = destinationProps, + ), options + ) + } + + fun group(scope: GroupScope.() -> Unit) { + val groupScope = GroupScope() + groupScope.scope() + group(groupScope.message, groupScope.options) + } + +} diff --git a/core/src/main/java/com/rudderstack/core/Base64Generator.kt b/core/src/main/java/com/rudderstack/core/Base64Generator.kt new file mode 100644 index 000000000..061ec1219 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Base64Generator.kt @@ -0,0 +1,29 @@ +/* + * Creator: Debanjan Chatterjee on 04/08/22, 11:39 PM Last modified: 04/08/22, 11:39 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +/** + * Base64 generator may vary over different java versions, thus we provide API to register the same + * + */ +fun interface Base64Generator { + /** + * Should generate Base64 equivalent + * + * @param string the string to encode + * @return return the encoded string + */ + fun generateBase64(string: String): String +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/BasicStorageImpl.kt b/core/src/main/java/com/rudderstack/core/BasicStorageImpl.kt new file mode 100644 index 000000000..4490f611a --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/BasicStorageImpl.kt @@ -0,0 +1,287 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.IdentifyTraits +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.lang.ref.WeakReference +import java.util.LinkedList +import java.util.Properties +import java.util.Queue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicReference + +private const val PROPERTIES_FILE_NAME = "config.properties" +private const val LIB_KEY_NAME = "libraryName" +private const val LIB_KEY_VERSION = "rudderCoreSdkVersion" +private const val LIB_KEY_PLATFORM = "platform" +private const val LIB_KEY_OS_VERSION = "os_version" + +@Suppress("ThrowableNotThrown") +class BasicStorageImpl @JvmOverloads constructor( + /** + * queue size should be greater than or equals [Storage.MAX_STORAGE_CAPACITY] + */ + private val queue: Queue = LinkedBlockingQueue(), +) : Storage { + + override lateinit var analytics: Analytics + private var configurationRef = AtomicReference(null) + + private val logger + get() = configurationRef.get()?.logger + private var backPressureStrategy = Storage.BackPressureStrategy.Drop + + private var _storageCapacity = Storage.MAX_STORAGE_CAPACITY + private var _maxFetchLimit = Storage.MAX_FETCH_LIMIT + + private var _dataChangeListeners = setOf>() + private var _isOptOut = false + private var _optOutTime = -1L + private var _optInTime = -1L + + private var _serverConfig: RudderServerConfig? = null + private var _traits: IdentifyTraits? = null +// private var _externalIds: List>? = null + + //library details + private val libDetails: Map by lazy { + try { + Properties().let { + it.load(FileInputStream(PROPERTIES_FILE_NAME)) + mapOf( + LIB_KEY_NAME to it.getProperty(LIB_KEY_NAME), + LIB_KEY_VERSION to it.getProperty(LIB_KEY_VERSION), + LIB_KEY_PLATFORM to it.getProperty(LIB_KEY_PLATFORM), + LIB_KEY_OS_VERSION to it.getProperty(LIB_KEY_OS_VERSION) + ) + } + } catch (ex: IOException) { + logger?.error(log = "Config fetch error", throwable = ex) + mapOf() + } + + } + + /** + * This queue holds the messages that are generated prior to destinations waking up + */ + private val startupQ = LinkedList() + + private val serverConfigFile = File("temp/rudder-analytics/server_config") + override fun setStorageCapacity(storageCapacity: Int) { + _storageCapacity = storageCapacity + } + + override fun setMaxFetchLimit(limit: Int) { + _maxFetchLimit = limit + } + + override fun saveMessage(vararg messages: Message) { + //a block to call data listener + val dataFailBlock: List.() -> Unit = { + _dataChangeListeners.forEach { + it.get()?.onDataDropped( + this, + IllegalArgumentException("Storage Capacity Exceeded") + ) + } + } + synchronized(this) { + val excessMessages = queue.size + messages.size - _storageCapacity + if (excessMessages > 0) { + if (backPressureStrategy == Storage.BackPressureStrategy.Drop) { + logger?.warn(log = "Max storage capacity reached, dropping last$excessMessages latest events") + + (messages.size - excessMessages).takeIf { + it > 0 + }?.apply { + queue.addAll(messages.take(this)) + //callback + messages.takeLast(excessMessages).run(dataFailBlock) + + } ?: messages.toList().run(dataFailBlock) + + } else { + logger?.warn(log = "Max storage capacity reached, dropping first$excessMessages oldest events") + val tobeRemovedList = ArrayList(excessMessages) + var counter = excessMessages + while (counter > 0) { + val item = queue.poll() + if (item != null) { + counter-- + tobeRemovedList.add(item) + } else + break + } + queue.addAll(messages.takeLast(_storageCapacity)) + //callback + tobeRemovedList.run(dataFailBlock) + } + } else { + queue.addAll(messages) + } + } + onDataChange() + } + + override fun setBackpressureStrategy(strategy: Storage.BackPressureStrategy) { + backPressureStrategy = strategy + } + + override fun deleteMessages(messages: List) { + //basic storage does not support async delete + deleteMessagesSync(messages) + } + + override fun addMessageDataListener(listener: Storage.DataListener) { + _dataChangeListeners = _dataChangeListeners + WeakReference(listener) + } + + override fun removeMessageDataListener(listener: Storage.DataListener) { + + _dataChangeListeners = _dataChangeListeners.filter { + it.get() != null && it.get() != listener + }.toSet() + + } + + override fun getData(offset: Int, callback: (List) -> Unit) { + callback.invoke( + synchronized(this) { + if (queue.size <= offset) emptyList() else + queue.toMutableList().takeLast(queue.size - offset) + .take(_maxFetchLimit).toList() + }) + } + + override fun getCount(callback: (Long) -> Unit) { + synchronized(this) { + queue.size.toLong().apply(callback) + } + } + + override fun getDataSync(offset: Int): List { + return synchronized(this) { + if (queue.size <= offset) emptyList() else queue.toList().takeLast(queue.size - offset) + .take(_maxFetchLimit) + } + } + + override fun saveServerConfig(serverConfig: RudderServerConfig) { + try { + if (!serverConfigFile.exists()) { + serverConfigFile.parentFile.mkdirs() + serverConfigFile.createNewFile() + } + val fos = FileOutputStream(serverConfigFile) + val oos = ObjectOutputStream(fos) + + oos.writeObject(serverConfig) + oos.flush() + fos.close() + + } catch (ex: Exception) { + logger?.error(log = "Server Config cannot be saved", throwable = ex) + } + } + + override fun saveOptOut(optOut: Boolean) { + _isOptOut = optOut + if (optOut) { + _optOutTime = System.currentTimeMillis() + } else + _optInTime = System.currentTimeMillis() + } + + override fun saveStartupMessageInQueue(message: Message) { + synchronized(this) { + startupQ.add(message) + } + } + + override fun clearStartupQueue() { + synchronized(this) { + startupQ.clear() + } + } + + override fun shutdown() { + //nothing much to do here + + } + + override fun clearStorage() { + synchronized(this) { + queue.clear() + startupQ.clear() + _traits = null + _serverConfig = null + serverConfigFile.delete() + } + } + + override fun deleteMessagesSync(messages: List) { + val messageIdsToRemove = messages.map { it.messageId } + synchronized(this) { + queue.removeAll { + it.messageId in messageIdsToRemove + } + } + onDataChange() + } + + + override val serverConfig: RudderServerConfig? + get() = _serverConfig ?: if (serverConfigFile.exists()) { + val fis = FileInputStream(serverConfigFile) + val oos = ObjectInputStream(fis) + _serverConfig = oos.readObject() as RudderServerConfig? + _serverConfig + } else null + + + override val startupQueue: List + get() = startupQ + + override val isOptedOut: Boolean + get() = _isOptOut + override val optOutTime: Long + get() = _optOutTime + override val optInTime: Long + get() = _optInTime + + override val libraryName: String + get() = libDetails[LIB_KEY_NAME] ?: "" + + override val libraryVersion: String + get() = libDetails[LIB_KEY_VERSION] ?: "" + + override val libraryPlatform: String + get() = libDetails[LIB_KEY_PLATFORM] ?: "" + + override val libraryOsVersion: String + get() = libDetails[LIB_KEY_OS_VERSION] ?: "" + + override fun updateConfiguration(configuration: Configuration) { + configurationRef.set(configuration) + } + + override fun toString(): String { + return "BasicStorageImpl(queue=$queue, _storageCapacity=$_storageCapacity, _maxFetchLimit=$_maxFetchLimit, _dataChangeListeners=$_dataChangeListeners, _isOptOut=$_isOptOut, _optOutTime=$_optOutTime, _optInTime=$_optInTime, _serverConfig=$_serverConfig, _traits=$_traits, libDetails=$libDetails, serverConfigFile=$serverConfigFile)" + } + + private fun onDataChange() { + synchronized(this) { + _dataChangeListeners.forEach { + it.get()?.onDataChange() + } + + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/Callback.kt b/core/src/main/java/com/rudderstack/core/Callback.kt new file mode 100644 index 000000000..ef50b46e8 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Callback.kt @@ -0,0 +1,31 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message + +/** + * Callback invoked when the client library is done processing a message. + * + * + * Methods may be called on background threads, implementations must implement their own + * synchronization if needed. Implementations should also take care to make the methods + * non-blocking. + */ +interface Callback { + /** + * Invoked when the message is successfully uploaded to Rudder. + * + * + * Note: The Rudder HTTP API itself is asynchronous, so this doesn't indicate whether the + * message was sent to all integrations or not — just that the message was sent to the Rudder API + * and will be sent to integrations at a later time. + */ + fun success(message: Message?) + + /** + * Invoked when the library fails sending a message. The message is still stored in DB + * + * + * This could be due to exhausting retries, or other unexpected errors. Use the `throwable` provided to take further action. + */ + fun failure(message: Message?, throwable: Throwable?) +} diff --git a/core/src/main/java/com/rudderstack/core/ConfigDownloadService.kt b/core/src/main/java/com/rudderstack/core/ConfigDownloadService.kt new file mode 100644 index 000000000..127887eb9 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/ConfigDownloadService.kt @@ -0,0 +1,33 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.RudderServerConfig + +/** + * Download config for SDK. + * Config aids in usage of device mode plugins. + * Do not add this plugin to Analytics using [Analytics.addInfrastructurePlugin] method. + * This [InfrastructurePlugin] should be sent as constructor params to [Analytics] instance. + * + */ +interface ConfigDownloadService : InfrastructurePlugin { + /** + * Fetches the config from the server + */ + fun download( + callback: (success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit + ) + + /** + * These listeners are attached with an optional replay argument. + * replay specifies how many old events will be broadcasted to the listener. + * Making it 0 will make the listener listen to only future downloads. + * + */ + fun addListener(listener: Listener, replay: Int) + fun removeListener(listener: Listener) + + @FunctionalInterface + fun interface Listener{ + fun onDownloaded(success: Boolean) + } +} diff --git a/core/src/main/java/com/rudderstack/core/Configuration.kt b/core/src/main/java/com/rudderstack/core/Configuration.kt new file mode 100644 index 000000000..4b137959c --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Configuration.kt @@ -0,0 +1,41 @@ +package com.rudderstack.core + +import com.rudderstack.core.internal.KotlinLogger +import com.rudderstack.rudderjsonadapter.JsonAdapter +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * The `Configuration` class defines the settings and parameters used to configure the RudderStack analytics SDK. + * This class is open for inheritance to allow for customization and extension. + * + */ +open class Configuration( + open val jsonAdapter: JsonAdapter, + open val options: RudderOption = RudderOption(), + open val flushQueueSize: Int = FLUSH_QUEUE_SIZE, + open val maxFlushInterval: Long = MAX_FLUSH_INTERVAL, + // changing the value post source config download has no effect + open val shouldVerifySdk: Boolean = false, + open val gzipEnabled: Boolean = true, + // changing the value post source config download has no effect + open val sdkVerifyRetryStrategy: RetryStrategy = RetryStrategy.exponential(), + open val dataPlaneUrl: String = DATA_PLANE_URL, + open val controlPlaneUrl: String = CONTROL_PLANE_URL, + open val logger: Logger = KotlinLogger(), + open val analyticsExecutor: ExecutorService = Executors.newSingleThreadExecutor(), + open val networkExecutor: ExecutorService = Executors.newCachedThreadPool(), + open val base64Generator: Base64Generator = RudderUtils.defaultBase64Generator, +) { + companion object { + // default flush queue size for the events to be flushed to server + const val FLUSH_QUEUE_SIZE = 30 + + // default timeout for event flush + // if events are registered and flushQueueSize is not reached + // events will be flushed to server after maxFlushInterval millis + const val MAX_FLUSH_INTERVAL = 10 * 1000L //10 seconds + const val DATA_PLANE_URL = "https://hosted.rudderlabs.com" + const val CONTROL_PLANE_URL = "https://api.rudderstack.com/" + } +} diff --git a/core/src/main/java/com/rudderstack/core/Controller.kt b/core/src/main/java/com/rudderstack/core/Controller.kt new file mode 100644 index 000000000..8be56642c --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Controller.kt @@ -0,0 +1,159 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message + +/** + * Handles all messages, assorting the plugins, keeping track of cache, to name a few of it's + * duties + * + */ +interface Controller { + /** + * Update the [Configuration] object to be used for all subsequent calls + * + * @param configurationScope Update the current configuration with this scope to + * return the updated configuration + */ + fun applyConfiguration(configurationScope: Configuration.() -> Configuration) + + /** + * Applies a closure method to all available Plugins + * Can break the system if not properly constructed. + * Check for the plugin type and apply only to plugins that seem necessary + * + * @param closure A method to be run on each plugin + */ + fun applyMessageClosure(closure : Plugin.() -> Unit) + /** + * Applies a closure method to all available [InfrastructurePlugin] + * Can break the system if not properly constructed. + * + * @param closure A method to be run on each [InfrastructurePlugin] + */ + fun applyInfrastructureClosure(closure : InfrastructurePlugin.() -> Unit) + + + /** + * Opt out from analytics and usage monitoring. No further data will be sent once set true + * @param optOut True to stop analytics data collection, false otherwise + */ + fun optOut(optOut : Boolean) + + /** + * Intended to be called by other Rudderstack Modules. Not meant for standard SDK usage. + */ + fun updateSourceConfig() + + /** + * Is opted out from analytics + */ + val isOptedOut : Boolean + + val currentConfiguration : Configuration? + val storage:Storage + + val dataUploadService:DataUploadService + val configDownloadService:ConfigDownloadService? + + /** + * The write key + * In case of multiple instances, this key is used to differentiate between them + */ + val writeKey: String + + fun addPlugin(vararg plugins: Plugin) + /** + * Custom plugins to be removed. + * + * @param plugin [Plugin] object + * @return true if successfully removed false otherwise + */ + fun removePlugin(plugin: Plugin) : Boolean + + fun addInfrastructurePlugin(vararg plugins: InfrastructurePlugin) + /** + * Infrastructure plugins to be removed. + * + * @param plugin [InfrastructurePlugin] object + * @return true if successfully removed false otherwise + */ + fun removeInfrastructurePlugin(plugin: InfrastructurePlugin) : Boolean + /** + * Submit a [Message] for processing. + * The message is taken up by the controller and it passes through the set of timelines defined. + * Refrain from using this unless you are sure about it. Use other utility methods for + * [Analytics.track], [Analytics.screen], [Analytics.identify], [Analytics.alias], [Analytics.group] + * In case of external ids, custom contexts and integrations passed in message as well as in options, + * the ones in options will replace those of message. + * + * @param message A [Message] object up for submission + * @param options Individual [RudderOption] for this message. Only applicable for this message + * @param lifecycleController LifeCycleController related to this message, null for default implementation + */ + fun processMessage(message: Message, options: RudderOption?, lifecycleController: LifecycleController? = null) + + /** + * Add a [Callback] for getting notified when a message is processed + * + * @param callback An object of [Callback] + */ + fun addCallback(callback: Callback) + + /** + * Removes an added [Callback] + * @see addCallback + * + * @param callback The callback to be removed + */ + fun removeCallback(callback: Callback) + + /** + * Removes all added [Callback] + * @see addCallback + * + */ + fun removeAllCallbacks() + /** + * Flush the remaining data from storage. + * However flush returns immediately if analytics is shutdown + */ + fun flush() + /** + * This blocks the thread till events are flushed. + * Users should prefer [flush] + * + */ + fun blockingFlush() : Boolean + //fun reset() + /** + * Shuts down the Analytics. Once shutdown, a new instance needs to be created. + * All executors and plugins to be shutdown. + * This isn't an instant operation. It might take some time to complete. + * Executors will finish executing the jobs they have taken + * + */ + fun shutdown() + + /** + * true if [shutdown] is called, false otherwise + */ + val isShutdown : Boolean + + /** + * The logger set upfront or default [Logger] + */ + val logger : Logger + + /** + * clears the storage of all data + * + */ + fun clearStorage() + + /** + * Resets the device mode destinations. Resets any traits and external ids attached to context + * + */ + fun reset() + +} diff --git a/core/src/main/java/com/rudderstack/core/DataUploadService.kt b/core/src/main/java/com/rudderstack/core/DataUploadService.kt new file mode 100644 index 000000000..f92979c7e --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/DataUploadService.kt @@ -0,0 +1,40 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message +import com.rudderstack.web.HttpResponse + +/** + * Class to handle data upload to server. + * Pass this instance to [Analytics] constructor to enable data upload. + * If added again using [Analytics.addInfrastructurePlugin] method, + * data will be sent through all instances implementing this interface. + * + */ +interface DataUploadService : InfrastructurePlugin { + + fun addHeaders(headers: Map) + + /** + * Uploads data to cloud + * + * @param data The list of messages to upload + * @param extraInfo If any data needs to be added to the body + * @param callback Callback providing either success or failure status of upload + */ + fun upload( + data: List, + extraInfo: Map? = null, + callback: (response: HttpResponse) -> Unit + ) + + /** + * Uploads data synchronously + * + * @param data The list of messages to upload + * @param extraInfo If any data needs to be added to the body + * @return status of upload, true if success, false otherwise + */ + fun uploadSync(data: List, extraInfo: Map? = null): + HttpResponse? + +} diff --git a/core/src/main/java/com/rudderstack/core/DestinationConfig.kt b/core/src/main/java/com/rudderstack/core/DestinationConfig.kt new file mode 100644 index 000000000..f60d215d0 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/DestinationConfig.kt @@ -0,0 +1,91 @@ +/* + * Creator: Debanjan Chatterjee on 13/01/22, 3:55 PM Last modified: 13/01/22, 3:55 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +/** + * Stores device mode destinations state. + * -- configurations used to activate sending data to destination sdks + * Is not applicable for cloud mode. + * Generally device mode destination sdks initialize asynchronously and sending data to those + * should be halted till these sdks are ready. + * Do not confuse this with sdk "enabled" This is for keeping track of initialization of + * enabled destinations + * @property integrationMap Denotes the integration along with it's integration state. + */ + +internal class DestinationConfig(private val integrationMap: Map = hashMapOf()) { + /** + * Assigns a particular integration to it's state. + * In case the integration is not present in the map, it's added along with it's state. + * + * @param integration The name of the integration. This should be same as [DestinationPlugin.name] + * @param initialized true if the integration is initialized, false otherwise + * @return A new [DestinationConfig] object + */ + fun withIntegration(integration: String, initialized: Boolean = false): DestinationConfig { + return withIntegrations(mapOf(integration to initialized)) + } + + /** + * @see withIntegration + * + * @param integrationsMap Add new states of integrations + * @return A new [DestinationConfig] object with updated integrations + */ + fun withIntegrations(integrationsMap: Map): DestinationConfig { + return copy(integrationMap + integrationsMap) + } + + /** + * Checks if the integration is ready. + * + * @param integration The name of the integration provided in [DestinationPlugin.name] + * @return true if ready else false + */ + fun isIntegrationReady(integration: String): Boolean { + return integrationMap[integration]?: false + } + + /** + * Get names of integrations which are ready + * + * @return List of names of the integrations, matching to [DestinationPlugin.name] + */ + fun getReadyIntegrations(): List { + return integrationMap.filter { + it.value + }.mapTo(ArrayList(integrationMap.size)) { + it.key + } + } + + /** + * Check if all integrations are ready. + */ + val allIntegrationsReady: Boolean + get() = integrationMap.values.filter { !it }.isNullOrEmpty() + + private fun copy(integrationMap: Map) = DestinationConfig(integrationMap) + + /** + * Remove the destination from destination config, so it won't be accounted for. + * + * @param plugin The name of the [DestinationPlugin] + */ + fun removeIntegration(plugin: String): DestinationConfig { + return copy(integrationMap.toMutableMap().apply { remove(plugin) }) + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/DestinationPlugin.kt b/core/src/main/java/com/rudderstack/core/DestinationPlugin.kt new file mode 100644 index 000000000..66de1f632 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/DestinationPlugin.kt @@ -0,0 +1,137 @@ +package com.rudderstack.core + +import com.rudderstack.core.DestinationPlugin.DestinationInterceptor +import com.rudderstack.core.models.Message + +/** + * Implement [BaseDestinationPlugin] instead to avoid writing boiler plate code + * Destination Plugins are those plugin extensions that are used to send data to device mode + * destinations. + * They are typical [Plugin], with the following differences. + * [Plugin.intercept] is called with a copy of the original message. + * [DestinationInterceptor] works on this copied message. + * But when [Plugin.Chain.proceed] is called, internally the copied message is replaced by the + * original message thus discarding all the changes DestinationPlugin and it's sub-plugins did on the + * message. + * This is to safeguard against changes for one destination being propagated to other destination. + * For propagating changes in Message, use general plugin. + * @param T The Destination type associated to this Plugin + */ +interface DestinationPlugin : Plugin { + /** + * Destination definition name as given in control plane + */ + val name: String + + /** + * Returns whether this plugin is ready to accept events. + * Closely linked to [onReadyCallbacks] + */ + val isReady: Boolean + val subPlugins: List + val onReadyCallbacks: List<(T?, Boolean) -> Unit> +// get() = ArrayList(field) + + /** + * To add a sub plugin to a main plugin. + * The order will be + * internal-plugins --> custom-plugins -> ... ->cloud plugin->sub-plugins of device-mode-1 -> + * device mode-plugin-1 --> sub-plugins of device-mode-2 --> device-mode-plugin-2 .... + * + * @param plugin A plugin object. + * @see DestinationInterceptor + */ + fun addSubPlugin(plugin: DestinationInterceptor) + + /** + * Called when the device destination is ready to accept requests + * + * @param callbackOnReady called with the destination specific class object and + * true or false depending on initialization success or failure + */ + fun addIsReadyCallback(callbackOnReady: (T?, isUsable: Boolean) -> Unit) + + /** + * Marker Interface for sub-plugins that can be added to each individual plugin. + * This is to discourage developers to intentionally/unintentionally adding main plugins as sub + * plugins. + * + * Sub-plugins are those which intercepts the data prior to it reaches the main plugin code. + * These plugins only act on the main-plugin that it is added to. + * + */ + interface DestinationInterceptor : Plugin + + /** + * Called when flush is triggered. + * + */ + fun flush() {} +} + +/** + * [DestinationPlugin] with filled in boiler plate code. + * Preferred way of constructing [DestinationPlugin] is by sub-classing it. + * By default [isReady] is false + * Call [setReady] when initialised + * + * @param T type of destination in [Plugin] + * @property name Name of plugin, used to filter plugins + */ +abstract class BaseDestinationPlugin(override val name: String) : DestinationPlugin { + private var _isReady = false + private var _subPlugins = listOf() + private var _onReadyCallbacks = listOf<(T?, Boolean) -> Unit>() + + override val subPlugins: List + get() = _subPlugins + + override val onReadyCallbacks: List<(T?, Boolean) -> Unit> + get() = _onReadyCallbacks + + override val isReady: Boolean + get() = _isReady + + + override fun addSubPlugin(plugin: DestinationPlugin.DestinationInterceptor) { + _subPlugins = _subPlugins + plugin + } + + override fun addIsReadyCallback(callbackOnReady: (T?, isUsable: Boolean) -> Unit) { + _onReadyCallbacks = _onReadyCallbacks + callbackOnReady + } + + fun setReady(isReady: Boolean, destinationInstance: T? = null) { + _isReady = isReady + _onReadyCallbacks.forEach { + it.invoke(destinationInstance, isReady) + } + } + + companion object { + + /** + * Constructs an destination for a lambda. This compact syntax is most useful for inline + * interceptors. + * By default isReady is true if creating instance this way + * Used for simple destinations. + * By default [isReady] is false + * Call [setReady] when initialised + * ```kotlin + * val plugin = DestinationPlugin("name") { chain: Plugin.Chain -> + * chain.proceed(chain.request()) + * } + * ``` + */ + inline operator fun invoke( + name: String, + crossinline block: (chain: Plugin.Chain) -> Message + ): BaseDestinationPlugin = + object : BaseDestinationPlugin(name) { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + return block(chain) + } + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/Dsl.kt b/core/src/main/java/com/rudderstack/core/Dsl.kt new file mode 100644 index 000000000..76a2c3e2f --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Dsl.kt @@ -0,0 +1,279 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.GroupTraits +import com.rudderstack.core.models.IdentifyTraits +import com.rudderstack.core.models.MessageDestinationProps +import com.rudderstack.core.models.TrackProperties +import com.rudderstack.core.models.* + +class TrackScope internal constructor() : MessageScope() { + private var eventName : String? = null + private var properties: TrackProperties? = null + private var traits: Map? = null + + fun trackProperties(scope: MapScope.() -> Unit){ + val propertiesScope = MapScope(properties) + propertiesScope.scope() + properties = propertiesScope.map + } + fun event(scope: StringScope.() -> Unit){ + val eventScope = StringScope() + eventScope.scope() + eventName = eventScope.value + } + fun event(name: String){ + eventName = name + } + + fun traits(scope: MapScope.() -> Unit){ + val traitsScope = MapScope(traits) + traitsScope.scope() + traits = traitsScope.map + } + override val message: TrackMessage + get() = TrackMessage.create(eventName?: throw IllegalArgumentException("No event name for track"), + RudderUtils.timeStamp, properties, userId = userId, destinationProps = destinationProperties) + +} +class ScreenScope internal constructor() : MessageScope() { + private var screenName : String? = null + private var category : String? = null + private var screenProperties: TrackProperties? = null + + fun screenProperties(scope: MapScope.() -> Unit){ + val propertiesScope = MapScope(screenProperties) + propertiesScope.scope() + screenProperties = propertiesScope.map + } + fun screenName(scope: StringScope.() -> Unit){ + val eventScope = StringScope() + eventScope.scope() + screenName = eventScope.value + } + fun screenName(name: String){ + screenName = name + } + fun category(scope: StringScope.() -> Unit){ + val eventScope = StringScope() + eventScope.scope() + category = eventScope.value + } + fun category(name: String){ + category = name + } + override val message: ScreenMessage + get() = ScreenMessage.create(screenName ?: throw IllegalArgumentException("Screen name is not provided for screen event"), + RudderUtils.timeStamp, category = category, + anonymousId = anonymousId, + properties = screenProperties, userId = userId, destinationProps = destinationProperties) + +} +class IdentifyScope internal constructor() : MessageScope() { + private var traits: IdentifyTraits? = null + private var userID: String? = null + + fun traits(scope: MapScope.() -> Unit){ + val traitsScope = MapScope(traits) + traitsScope.scope() + traits = traitsScope.map + } + override val message: IdentifyMessage + get() = IdentifyMessage.create( + userId = userID, + anonymousId = anonymousId, + timestamp = RudderUtils.timeStamp, + traits = traits, + destinationProps = destinationProperties) + +} +class AliasScope internal constructor() : MessageScope() { + private var newID: String? = null + + private var traits: Map? = null + + fun newId(scope: StringScope.() -> Unit){ + val titleScope = StringScope() + titleScope.scope() + newID = titleScope.value + } + fun newId(newId : String){ + newID = newId + } + + fun traits(scope: MapScope.() -> Unit){ + val traitsScope = MapScope(traits) + traitsScope.scope() + traits = traitsScope.map + } + + override val message: AliasMessage + get() = AliasMessage.create(timestamp = RudderUtils.timeStamp, userId = newID, + anonymousId = anonymousId, + previousId = userId?:anonymousId, traits = traits, destinationProps = + destinationProperties, + ) + +} +class GroupScope internal constructor() : MessageScope() { + private var groupId : String? = null + private var traits: GroupTraits? = null + fun traits(scope: MapScope.() -> Unit){ + val groupScope = MapScope(traits) + groupScope.scope() + traits = groupScope.map + } + + + fun groupId(scope: StringScope.() -> Unit){ + val groupScope = StringScope() + groupScope.scope() + groupId = groupScope.value + } + fun groupId(id: String){ + groupId = id + } + + override val message: GroupMessage + get() = GroupMessage.create( + timestamp = RudderUtils.timeStamp, userId = userId, + anonymousId = anonymousId, + groupId = groupId, groupTraits = traits + , destinationProps = destinationProperties) + +} + +@MessageScopeDslMarker +abstract class MessageScope internal constructor(/*private val analytics: Analytics*/) { + private var _options: RudderOption? = null + internal val options + get() = _options + + private var _destinationProperties: MessageDestinationProps? = null + protected val destinationProperties + get() = _destinationProperties + protected var anonymousId: String? = null + protected var userId: String? = null + fun rudderOptions(scope: RudderOptionsScope.() -> Unit) { + val optionsScope = RudderOptionsScope() + optionsScope.scope() + _options = optionsScope.rudderOption + } + + fun destinationProperties(scope: MapScope>.() -> Unit){ + val destinationPropsScope = MapScope(_destinationProperties) + destinationPropsScope.scope() + _destinationProperties = destinationPropsScope.map + } + fun userId(scope: StringScope.() -> Unit){ + val titleScope = StringScope() + titleScope.scope() + userId = titleScope.value + } + fun userId(userId : String){ + this.userId = userId + } + fun anonymousId(scope: StringScope.() -> Unit){ + val anonymousIdScope = StringScope() + anonymousIdScope.scope() + anonymousId = anonymousIdScope.value + } + fun anonymousId(id: String){ + anonymousId = id + } + internal abstract val message: T + /*internal fun send(){ + analytics.processMessage(message, options) + }*/ + +} + +class StringScope internal constructor(){ + private var _value : String? = null + val value + get() = _value + + operator fun String.unaryPlus(){ + _value = this + } +} +@OptionsScopeDslMarker +class RudderOptionsScope internal constructor() { + + // private var rudderOptionsBuilder: RudderOptions.Builder = RudderOptions.Builder() + internal val rudderOption: RudderOption + get() = _rudderOption + private val _rudderOption: RudderOption by lazy { + RudderOption() + } + fun externalId(type: String, id: String) { + _rudderOption.putExternalId(type, id) + } + + fun integration(destinationKey: String, enabled: Boolean) { + _rudderOption.putIntegration(destinationKey, enabled) + } + + fun integration(destination: BaseDestinationPlugin<*>, enabled: Boolean) { + _rudderOption.putIntegration(destination, enabled) + } + + fun customContexts(key: String, context: Map) { + _rudderOption.putCustomContext(key, context) + } + + +} + +class CollectionsScope +internal constructor(private var _collection: Collection?) { + operator fun E.unaryPlus() { + _collection = _collection?.let { it + this } ?: listOf(this) + } + + operator fun Collection.unaryPlus() { + _collection = _collection?.let { it + this } ?: this + } + + infix fun add(item: E) { + +item + } + + infix fun add(items: Collection) { + +items + } + + internal val collection + get() = _collection +} + +class MapScope +internal constructor(private var _map: Map?) { + operator fun Pair.unaryPlus() { + _map = _map optAdd this + } + + operator fun Map.unaryPlus() { + _map = _map optAdd this + } + + infix fun add(item: Pair) { + +item + } + + infix fun add(items: Map) { + +items + } + + internal val map + get() = _map +} + + +@DslMarker +annotation class MessageScopeDslMarker + +@DslMarker +annotation class PropertyScopeDslMarker + +@DslMarker +annotation class OptionsScopeDslMarker diff --git a/core/src/main/java/com/rudderstack/core/Exceptions.kt b/core/src/main/java/com/rudderstack/core/Exceptions.kt new file mode 100644 index 000000000..d7e438e39 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Exceptions.kt @@ -0,0 +1,23 @@ +/* + * Creator: Debanjan Chatterjee on 22/03/22, 3:18 PM Last modified: 22/03/22, 3:18 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +/** + * Thrown when an event misses any property + * + * + * @param message String passed to [Throwable] + */ +class MissingPropertiesException(message: String) : Throwable(message) \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/InfrastructurePlugin.kt b/core/src/main/java/com/rudderstack/core/InfrastructurePlugin.kt new file mode 100644 index 000000000..61587f2ed --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/InfrastructurePlugin.kt @@ -0,0 +1,45 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.RudderServerConfig + +/** + * While [Plugin] is mostly used for message processing, [InfrastructurePlugin] is used for + * implementing infrastructure related tasks. + * Infrastructure Plugins are generally independent of event processing. + * + */ +interface InfrastructurePlugin { + + var analytics: Analytics + + fun setup(analytics: Analytics) { + this.analytics = analytics + } + + fun shutdown() {} + fun updateConfiguration(configuration: Configuration) { + //optional method + } + + fun updateRudderServerConfig(serverConfig: RudderServerConfig) { + //optional method + } + + /** + * Pause the proceedings if applicable, for example data upload service can halt the upload + */ + fun pause() { + //optional-method + } + + /** + * Resume the proceedings had the plugin been paused + */ + fun resume() { + //optional-method + } + + fun reset() { + //optional method + } +} diff --git a/core/src/main/java/com/rudderstack/core/LifecycleController.kt b/core/src/main/java/com/rudderstack/core/LifecycleController.kt new file mode 100644 index 000000000..397222de5 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/LifecycleController.kt @@ -0,0 +1,34 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message + +/** + * Handles the lifecycle of a message. + * Most importantly aligns the plugins. + * Acts as a bridge between application layer and internal business layer. + * Might also be referred as LCC later on + * + */ +interface LifecycleController { + /** + * Each message is connected to it's Lifecycle Controller. Returns the associated message + */ + val message: Message + + /** + * Separate options can be added for each message, null if there are no specific options + *//* + val options : RudderOptions?*/ + + /** + * Associated list of plugins + */ + val plugins: List + + /** + * The message is up for processing. + * Plugins will be applied to it. + * + */ + fun process() +} diff --git a/core/src/main/java/com/rudderstack/core/Logger.kt b/core/src/main/java/com/rudderstack/core/Logger.kt new file mode 100644 index 000000000..16a165e05 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Logger.kt @@ -0,0 +1,71 @@ +package com.rudderstack.core + +typealias RudderLogLevel = Logger.LogLevel + +/** + * Logger interface. + * Contains methods for different scenarios + * + */ +const val DEFAULT_TAG = "Rudder-Analytics" +interface Logger { + companion object { + @JvmField + val DEFAULT_LOG_LEVEL = LogLevel.NONE + } + + /** + * Activate or deactivate logger based on choice. + * + * @param level should log anything greater or equal to that level. See [LogLevel] + */ + fun activate(level: LogLevel) + + fun info(tag: String = DEFAULT_TAG, log: String) + + fun debug(tag: String = DEFAULT_TAG, log: String) + + fun warn(tag: String = DEFAULT_TAG, log: String) + + fun error(tag: String = DEFAULT_TAG, log: String, throwable: Throwable? = null) + + /** + * Level based on priority. Higher the number, greater the priority + * + * @property level priority for each type + */ + enum class LogLevel { + DEBUG, + INFO, + WARN, + ERROR, + NONE, + } + + val level: LogLevel + + object Noob : Logger { + override fun activate(level: LogLevel) { + // do nothing + } + + override fun info(tag: String, log: String) { + // do nothing + } + + override fun debug(tag: String, log: String) { + // do nothing + } + + override fun warn(tag: String, log: String) { + // do nothing + } + + override fun error(tag: String, log: String, throwable: Throwable?) { + // do nothing + } + + override val level: LogLevel + get() = DEFAULT_LOG_LEVEL + } +} diff --git a/core/src/main/java/com/rudderstack/core/Plugin.kt b/core/src/main/java/com/rudderstack/core/Plugin.kt new file mode 100644 index 000000000..93402ba6c --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Plugin.kt @@ -0,0 +1,113 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig + +/** + * Observes, modifies, and potentially short-circuits requests going out and the corresponding + * responses coming back in. Typically plugins transforms or logs the data sent over to destinations. + * + */ +interface Plugin { + + var analytics: Analytics + + fun setup(analytics: Analytics) { + this.analytics = analytics + } + + /** + * Joins ("chains") the plugins for a particular message + * + */ + interface Chain { + /** + * Returns the message the Chain is associated to. + * + * @return Message + */ + fun message(): Message + + /** + * This behaves differently for destination plugins. For message + * plugins, this is the original message that was passed to the chain. + * For, Destination plugins this is prior to being copied and + * intercepted by Sub Plugins, but will reflect the changes + * made by other plugins. + * [Chain.proceed] should be called with this message so as to discard any + * alteration to the message by [DestinationPlugin] + */ + val originalMessage: Message + + /** + * Indicates that processing of this plugin is over and now the message is ready to be taken forward. + * If changes made to the message object is local to plugin and is not intended to be moved forward, + * then the old message should be proceeded. In these cases, it is advised to create a deep copy + * of message. Otherwise changes made in Maps and Lists might be propagated through, unintentionally. + * + * In case the change is intended to be forwarded, proceed with the changed message. + * + * @param message The message to be processed + * @return The processed message that will be provided to later interceptors. + */ + fun proceed(message: Message): Message + + /** + * Get the list of plugins this Chain operates on + * + * @return the set of plugins that this Chain operates on + */ + val plugins: List + + /** + * Index of the plugin that is being operated. + * Generally if called from inside a plugin, this denotes the index of the plugin + */ + val index: Int + + /** + * Create a copy of [Chain] with updated set of plugins. + * + * @param plugins + */ + fun with(plugins: List): Chain + + } + /*companion object { + */ + /** + * Constructs an interceptor for a lambda. This compact syntax is most useful for inline + * interceptors. + * + * ```kotlin + * val plugin = Plugin { chain: Plugin.Chain -> + * chain.proceed(chain.request()) + * } + * ``` + *//* + inline operator fun invoke(crossinline block: (chain: Chain) -> Message): Plugin = + Plugin { block(it) } + }*/ + fun intercept(chain: Chain): Message + + /** + * Called when settings is updated + * + * @param configuration [Configuration] globally set for the sdk + */ + fun updateConfiguration(configuration: Configuration) {} + + fun updateRudderServerConfig(config: RudderServerConfig) {} + + /** + * Called when shutDOwn is triggered in [Analytics] + * refactor to shutdown + */ + fun onShutDown() {} + + /** + * Called when reset is triggered in Analytics + * + */ + fun reset() {} +} diff --git a/core/src/main/java/com/rudderstack/core/Properties.kt b/core/src/main/java/com/rudderstack/core/Properties.kt new file mode 100644 index 000000000..9ad3b7846 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Properties.kt @@ -0,0 +1,38 @@ +package com.rudderstack.core + +/** + * Utility extensions for simplifying APIs + + **/ +/** + * Can be used in [Analytics.track] as + * ```kotlin + * Analytics.track("dummy_event", mapOf("some_property", value).addCurrency("dollars")) + * ``` + * + * @param value The currency used. + */ +fun Map.addCurrency(value: String) = this + Pair("currency", value) + +/** + * Can be used in [Analytics.track] as + * ```kotlin + * Analytics.track("dummy_event", mapOf("some_property", value).addCurrency("dollars").addRevenue("500")) + * ``` + * + * @param value The revenue generated. + */ +fun Map.addRevenue(value: String) = this + Pair("revenue", value) + +fun Map.addCategory(category: String) = this + ("category" to category) + +/** + * Used for adding advertisingId to context + * + * @param advertisingId The advertising id associated to the application + */ +fun Map.putAdvertisingId(advertisingId: String) = this + ("advertisingId" to advertisingId) +fun Map.putDeviceToken(advertisingId: String) = this + ("advertisingId" to advertisingId) + + +fun Map.with(vararg keyPropertyPair: Pair) = this + keyPropertyPair diff --git a/core/src/main/java/com/rudderstack/core/RetryStrategy.kt b/core/src/main/java/com/rudderstack/core/RetryStrategy.kt new file mode 100644 index 000000000..38afe2a9b --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/RetryStrategy.kt @@ -0,0 +1,157 @@ +/* + * Creator: Debanjan Chatterjee on 23/01/22, 11:37 PM Last modified: 23/01/22, 11:37 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import java.lang.ref.WeakReference +import java.util.concurrent.Future +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +/** + * A retry strategy mechanism. + * Users can either use statically provided exponential strategy or create a strategy of their own + * + */ +fun interface RetryStrategy { + + companion object { + /** + * An utility function to get [ExponentialRetryStrategy]. + * + * @param maxAttempts + * @return [ExponentialRetryStrategy] + */ + @JvmStatic + @JvmOverloads + fun exponential( + maxAttempts: Int = 5 + ) = ExponentialRetryStrategy(maxAttempts) + + } + + fun Analytics.perform(work: Analytics.() -> Boolean, listener: (success: Boolean) -> Unit): + CancellableJob + interface CancellableJob { + /** + * Cancels the job + * By norm, it should cancel the job and invoke the listener with false + * + */ + fun cancel() + + fun isDone(): Boolean + } + + /** + * A retry strategy that increases the waiting time by exponents of 2 + * The waiting time increases as (2^n),i.e 1,2,4,8.... + * + * @property maxAttempts The max number of attempts to make. It will occur at 2^nth second after + * (n-1)th attempt + */ + class ExponentialRetryStrategy internal constructor( + private val maxAttempts: Int, + ) : RetryStrategy { + + override fun Analytics.perform( + work: Analytics.() -> Boolean, listener: (success: Boolean) -> Unit + ): CancellableJob { + val impl = WeakReference(ExponentialRetryImpl(this, maxAttempts,work, listener)) + impl.get()?.start() + return object : CancellableJob { + override fun cancel() { + impl.get()?.cancel() + listener(false) + } + + override fun isDone(): Boolean { + return impl.get()?.isDone == true + } + } + } + + + inner class ExponentialRetryImpl internal constructor(private val analytics: Analytics, + private val maxAttempts: Int, private val work: Analytics.() -> Boolean, private val + listener: ( + success: Boolean + ) -> Unit + ) { + private var retryCount = AtomicInteger(0) + private var lastWaitTime = AtomicLong(0L) + private val isRunning = AtomicBoolean(false) + private val executorService = ScheduledThreadPoolExecutor(0) + private var lastFuture: WeakReference>? = null + private val _isDone = AtomicBoolean(false) + private val logger + get() = analytics.currentConfiguration?.logger + val isDone: Boolean + get() = _isDone.get() + fun start() { + if (isDone) { + logger?.warn( + "ExponentialRetryStrategy:", "RetryStrategyImpl is already shutdown" + ) + return + } + if (!isRunning.compareAndSet(false, true)) { + logger?.warn( + "ExponentialRetryStrategy:", "RetryStrategyImpl is already running" + ) + return + } + check(maxAttempts >= 0) { + "Max attempts needs to be at least 1" + } + check(retryCount.get() < 1) { + "perform() can be called only once. Create another instance for a new job." + } + scheduleWork() + } + + fun cancel() { + lastFuture?.get()?.takeIf { !it.isCancelled && !it.isDone }?.cancel(false) + executorService.shutdown() + _isDone.set(true) + } + + private fun scheduleWork() { + executorService.schedule({ + lastWaitTime.set((1 shl retryCount.getAndIncrement()) * 1000L) // 2 to the power of retry count + + val workDone = work.invoke(analytics) + if (workDone) { + listener.invoke(true) + cancel() + return@schedule + } else { + if (retryCount.get() >= maxAttempts) { + listener.invoke(false) + cancel() + return@schedule + } + scheduleWork() + } + }, lastWaitTime.get(), TimeUnit.MILLISECONDS).also { + lastFuture = WeakReference(it) + } + } + + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/RudderOption.kt b/core/src/main/java/com/rudderstack/core/RudderOption.kt new file mode 100644 index 000000000..9da1e35b3 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/RudderOption.kt @@ -0,0 +1,92 @@ +/* + * Creator: Debanjan Chatterjee on 28/12/21, 4:32 PM Last modified: 28/12/21, 4:25 PM + * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +private const val TYPE = "type" +private const val ID = "id" + +/** + * Can be set as global or customised options for each message. + * If no customised option is set for a message, global options will be used. + * Users can pass any object as values, but in case of complex classes, please check with the + * serializer/deserializer adapter being used + * @property externalIds External ids can be used for assigning extra ids for different destinations + * on the transformer side. + * @property integrations The integrations to which these messages will be delivered. If empty, the + * message will be delivered to all destinations added to Analytics. + * @property customContexts Custom context elements that are going to be sent with message + */ +class RudderOption { + val integrations: Map + get() = _integrations + private val _integrations = mutableMapOf() + val customContexts: Map + get() = _customContexts + private val _customContexts = mutableMapOf() + + val externalIds: List> + get() = _externalIds + private val _externalIds = mutableListOf>() + + fun putExternalId(type: String, id: String): RudderOption { + val existingExternalIdIndex = + _externalIds.indexOfFirst { it[TYPE]?.equals(type, ignoreCase = true) == true } + + if (existingExternalIdIndex != -1) { + // If the type exists, update the id + _externalIds[existingExternalIdIndex] = + _externalIds[existingExternalIdIndex].toMutableMap().apply { + this[ID] = id + } + } else { + // If the type does not exist, add a new externalId + _externalIds.add( + mapOf( + TYPE to type, + ID to id + ) + ) + } + return this + } + + fun putIntegration(destinationKey: String, enabled: Boolean): RudderOption { + _integrations[destinationKey] = enabled + return this + } + + fun putIntegration(destination: BaseDestinationPlugin<*>, enabled: Boolean): RudderOption { + _integrations[destination.name] = enabled + return this + } + + fun putCustomContext(key: String, context: Map): RudderOption { + _customContexts[key] = context + return this + } + + + override fun equals(other: Any?): Boolean { + return other is RudderOption && + other.externalIds == this.externalIds && + other.integrations == this.integrations && + other.customContexts == this.customContexts + } + + override fun hashCode(): Int { + return externalIds.hashCode() + integrations.hashCode() + customContexts.hashCode() + } + +} diff --git a/core/src/main/java/com/rudderstack/core/RudderUtils.kt b/core/src/main/java/com/rudderstack/core/RudderUtils.kt new file mode 100644 index 000000000..5ab11b507 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/RudderUtils.kt @@ -0,0 +1,65 @@ +/* + * Creator: Debanjan Chatterjee on 11/01/22, 11:07 PM Last modified: 05/11/21, 7:42 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import java.io.UnsupportedEncodingException +import java.text.SimpleDateFormat +import java.util.* + +object RudderUtils { + val defaultBase64Generator: Base64Generator by lazy { + Base64Generator { + Base64.getEncoder().encodeToString( + String.format(Locale.US, "%s:", it).toByteArray(charset("UTF-8")) + ) + } + } + + // range constants + /*const val MIN_CONFIG_REFRESH_INTERVAL = 1 + const val MAX_CONFIG_REFRESH_INTERVAL = 24 + const val MIN_SLEEP_TIMEOUT = 10 + const val MIN_FLUSH_QUEUE_SIZE = 1 + const val MAX_FLUSH_QUEUE_SIZE = 100*/ + internal const val MAX_EVENT_SIZE = 32 * 1024 // 32 KB + internal const val MAX_BATCH_SIZE = 500 * 1024 // 500 KB + val timeZone: String + get() { + val timeZone = TimeZone.getDefault() + return timeZone.id + } + val timeStamp: String + get() { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + formatter.timeZone = TimeZone.getTimeZone("GMT") + return formatter.format(Date()) + } + + internal fun toDateString(date: Date): String { + val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) + return date.let { + formatter.format(it) + } + } + + internal fun String.getUTF8Length(): Int { + return try { + this.toByteArray(Charsets.UTF_8).size + } catch (ex: UnsupportedEncodingException) { + -1 + } + } + +} diff --git a/core/src/main/java/com/rudderstack/core/State.kt b/core/src/main/java/com/rudderstack/core/State.kt new file mode 100644 index 000000000..30a5c9dc6 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/State.kt @@ -0,0 +1,103 @@ +/* + * Creator: Debanjan Chatterjee on 29/12/21, 5:38 PM Last modified: 24/11/21, 11:18 PM + * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import java.lang.ref.WeakReference + +/** + * Base class for specific grouped configuration. + * The value can be observed by adding observers. + * Observers are added as weak reference to counter memory leaks + * + * @param T The type of value the class holds + * @constructor + * + * + * @param initialValue An initial value can be supplied, which defaults to null + */ +abstract class State(initialValue: T? = null) { + + + private var _value: T? = initialValue + set(value) { + synchronized(this) { + val oldValue = field + field = value + // notifies observers as state changes. Initial value won't be notified + observers.forEach { + it.get()?.onStateChange(value, oldValue) + } + } + } + val value: T? + get() = _value + + private val observers: MutableSet>> = HashSet() + + /** + * Observe the value change + * + * @see Observer + * @param observer an instance of Observer + */ + fun subscribe(observer: Observer) { + synchronized(this){ + if (observers.firstOrNull { + it.get()?.equals(observer) == true + } != null) return + observers.add(WeakReference(observer)) + observer.onStateChange(value, null) + } + } + + /** + * update the value of the state. + * This value is going to be notified to all listed observers + * + * @param value New value of state + */ + fun update(value : T?){ + this._value = value + } + + fun removeObserver(observer: Observer){ + synchronized(this) { + observers.removeAll(observers.filter{ + //remove if observer ref is removed or observer is same as given one + it.get()?.equals(observer) ?: true + }.toSet()) + } + } + fun removeAllObservers(){ + synchronized(this) { + observers.clear() + } + } + + /** + * Observer interface for State + * + * @param T Type of value State holds + */ + fun interface Observer { + /** + * Called when state changes + * + * @param state New state + * @param previousState Old state - Not available if subscribed for first time + */ + fun onStateChange(state: T?, previousState: T?) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/Storage.kt b/core/src/main/java/com/rudderstack/core/Storage.kt new file mode 100644 index 000000000..1cbeaf87b --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Storage.kt @@ -0,0 +1,227 @@ +package com.rudderstack.core + +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig + +/** + * Do not call these methods directly + * These are meant to be called from implementation of [Controller] + * Requires specific implementation of how storage should be handled. + * Can be customised according to requirements. + * For custom modifications over queue based implementation one can extend [BasicStorageImpl] + * Since we intend to sync the database at regular intervals, hence data volumes handled by storage + * is pretty low. + * That is the reason are avoiding any selection arguments. + */ +interface Storage : InfrastructurePlugin{ + companion object { + const val MAX_STORAGE_CAPACITY = 2_000 + const val MAX_FETCH_LIMIT = 5_00 + } + + /** + * The max number of events that can be stored. + * Defaults to [MAX_STORAGE_CAPACITY] + * If storage overflows beyond this, data retention should be based on [BackPressureStrategy] + * @see setBackpressureStrategy + * @param storageCapacity + */ + fun setStorageCapacity(storageCapacity: Int = MAX_STORAGE_CAPACITY) + + /** + * The max number of [Message] that can be fetched at one go. + * Applies to [getData] and [DataListener.onDataChange] + * @param limit The number of messages to be set as limit, defaults to [MAX_FETCH_LIMIT] + */ + fun setMaxFetchLimit(limit: Int = MAX_FETCH_LIMIT) + + /** + * Platform specific implementation for saving [Message] + * Is called from the same executor the plugins are processed on. + * + * @param messages A single or multiple messages to be saved + */ + fun saveMessage(vararg messages: Message) + + /** + * Default back pressure strategy is [BackPressureStrategy.Drop] + * + * @param strategy [BackPressureStrategy] for queueing [Message] + */ + fun setBackpressureStrategy(strategy: BackPressureStrategy = BackPressureStrategy.Drop) + + /** + * Delete Messages, preferably when no longer required in an async manner + * + * @param messages [Message] objects ready to be removed from storage + */ + fun deleteMessages(messages: List) + + /** + * Add a data change listener that calls back when any changes to data is made. + * @see DataListener + * + * @param listener callback for the changed data + */ + fun addMessageDataListener(listener: DataListener) + + fun removeMessageDataListener(listener: DataListener) + + /** + * Asynchronous method to get the entire data present to a maximum of fetch limit + * @see setMaxFetchLimit + * + * @param offset offset the fetch by the given amount, i.e elements from offset to size-1 + * @param callback returns the list of [Message] + */ + fun getData(offset: Int = 0, callback: (List) -> Unit) + + /** + * Data count, analogous to count(*) + * + * @param callback + */ + fun getCount(callback : (Long) -> Unit) + + /** + * synchronous method to get the entire data present to a maximum of fetch limit + * @see setMaxFetchLimit + * @param offset offset the fetch by the given amount, i.e elements from offset to size-1 + * + * @return the list of messages. + */ + fun getDataSync(offset: Int = 0): List + + + + /** + * CPU extensive operation, better to offload to a different thread + * + * @param serverConfig SDK initialization data [RudderServerConfig] + */ + fun saveServerConfig(serverConfig: RudderServerConfig) + + /** + * CPU intensive operation. Might involve file access. + */ + val serverConfig: RudderServerConfig? + + /** + * Platform specific implementation of saving opt out choice. + * + * @param optOut Save opt out state + */ + fun saveOptOut(optOut: Boolean) + + /** + * [DestinationPlugin]s in general start up asynchronously, which means messages sent + * prior to their initialization might get dropped. Thus to counter that, [Message] objects + * received prior to all Destination Plugins initialization are stored in startup queue and + * replayed back. + * Here developers can provide their custom implementation of the startup queue. + * @param message [Message] objects that are being sent to destination plugins prior to their + * startup + */ + fun saveStartupMessageInQueue(message: Message) + + /** + * Clear startup queue + * + */ + fun clearStartupQueue() + + /** + * Clears any and every persistent storage. + * Depending on the implementation one can clear the database or + * remove preferences + * + */ + fun clearStorage() + /** + * Delete Messages, preferably when no longer required in an sync manner on the calling thread + * + * @param messages [Message] objects ready to be removed from storage + */ + fun deleteMessagesSync(messages: List) + + /** + * @see saveStartupMessageInQueue + * get the messages that were posted before all device mode destinations initialized. + */ + val startupQueue: List + + /** + * Get opted out state + */ + val isOptedOut: Boolean + + /** + * Returns opt out time instant if any else -1L + */ + val optOutTime: Long + + /** + * Returns opt in time instant if any else -1L + */ + val optInTime: Long + + /** + * Returns traits + */ +// val traits: IdentifyTraits? + + /** + * external ids persisted through identify calls. + * Any external ids passed in other events will be merged with this. + */ +// val externalIds: List>? + + + + val libraryName : String + val libraryVersion : String + val libraryPlatform : String + val libraryOsVersion : String + + /** + * Data Listener for data change and on data dropped. + * Data Change listener is called whenever there's a change in data count + * + * + */ + interface DataListener { + /** + * Called whenever there's a change in data count + * + */ + fun onDataChange() + + /** + * Called when data is dropped to adhere to the backpressure strategy. + * @see [BackPressureStrategy] + * @see setBackpressureStrategy + * + * @param messages List of messages that have been dropped + * @param error [Throwable] to enhance on the reason + */ + fun onDataDropped(messages: List, error: Throwable) + } + + /** + * Back pressure strategy for handling message queue + * + */ + enum class BackPressureStrategy { + /** + * Messages are dropped in case queue is full + * + */ + Drop, + + /** + * Keeps only the most recent item, oldest ones are dropped + * + */ + Latest + } +} diff --git a/core/src/main/java/com/rudderstack/core/Utilities.kt b/core/src/main/java/com/rudderstack/core/Utilities.kt new file mode 100644 index 000000000..e263a9a8d --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/Utilities.kt @@ -0,0 +1,117 @@ +/* + * Creator: Debanjan Chatterjee on 29/11/23, 6:09 pm Last modified: 16/11/23, 11:15 am + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +/** + * Operator to optionally add a nullable [Map] to callee + * usage + * val completeMap = mapOf("userId" to userID) optAdd someNullableMap + * + * @param value The nullable [Map] object to "add" + * @return The "sum" of both maps if [value] is not null, else this + */ +infix fun Map?.optAdd(value: Map?): Map { + return if (this == null) value ?: mapOf() else value?.let { + this + it + } ?: this +} + +/** + * See [optAdd] + * + */ +infix fun Map?.optAdd(value: Pair?): Map { + return if (this != null) value?.let { + this + it + } ?: this else value?.let { mapOf(it) } ?: mapOf() +} + + + +/** + * Operator to perform a block on it, only if it's not null + * Usage + * + * var someNullableMap : Map? = null + * ... + * fun someMethod(){ + * someNullableMap ifNotNull storage::saveMap + * } + * + * ... + * fun saveMap(map: Map){ + * ... + * } + * + * + * @param T The generic callee object which will serve as parameter to [block] + * @param R The return type expected from [block] + * @param block A lambda function that takes type T as parameter + * @return The result of [block] applied to "this" + */ +infix fun T?.ifNotNull(block: (T) -> R): R? { + return this?.let(block) +} + +/** + * Applies minus operation to two iterable containing maps, with respect to keys + * ``` + * val a = mapOf("1" to 1) + * val b = mapOf("2" to 3) + * val c = mapOf("4" to 4) + * val d = mapOf("2" to 5) + + * val list1 = listOf(a,b,c) + * val list2 = listOf(d) + * + * val diff = list1 minusWrtKeys list2 //[{1=1}, {4=4}] + * + * ``` + * + * @param operand + * @return + */ + +infix fun Iterable>.minusWrtKeys( + operand: + Iterable> +): List> { + operand.toSet().let { op -> + return this.filterNot { + op inWrtKeys it + } + } +} + +/** + * infix function to match "in" condition for a map inside a list of maps based on just keys. + * ``` + * val list = listOf( + * mapOf("1" to 1), + * mapOf("2" to 3), + * mapOf("4" to 4) + * ) + * list inWrtKeys mapOf("2", "2") //returns true + * ``` + * @param item The item to be checked + * @return true if the map with same keys are present, false otherwise + */ +infix fun Iterable>.inWrtKeys(item: Map): Boolean { + this.toSet().forEach { + if (it.keys.containsAll(item.keys)) + return true + } + return false +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/compat/AnalyticsBuilderCompat.java b/core/src/main/java/com/rudderstack/core/compat/AnalyticsBuilderCompat.java new file mode 100644 index 000000000..e605d3944 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/compat/AnalyticsBuilderCompat.java @@ -0,0 +1,96 @@ +/* + * Creator: Debanjan Chatterjee on 13/10/22, 12:29 PM Last modified: 13/10/22, 12:29 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.compat; + +import com.rudderstack.core.Analytics; +import com.rudderstack.core.BasicStorageImpl; +import com.rudderstack.core.ConfigDownloadService; +import com.rudderstack.core.Configuration; +import com.rudderstack.core.DataUploadService; +import com.rudderstack.core.Storage; +import com.rudderstack.core.internal.ConfigDownloadServiceImpl; +import com.rudderstack.core.internal.DataUploadServiceImpl; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; + +public class AnalyticsBuilderCompat { + private final String writeKey; + + private final Configuration configuration; + private Storage storage = new BasicStorageImpl(); + private DataUploadService dataUploadService = null; + private ConfigDownloadService configDownloadService = null; + + private Function1 shutdownHook = null; + private Function2 initializationListener; + + public AnalyticsBuilderCompat(String writeKey, Configuration configuration) { + this.writeKey = writeKey; + this.configuration = configuration; + } + + public AnalyticsBuilderCompat withDataUploadService(DataUploadService dataUploadService) { + this.dataUploadService = dataUploadService; + return this; + } + + public AnalyticsBuilderCompat withConfigDownloadService(ConfigDownloadService configDownloadService) { + this.configDownloadService = configDownloadService; + return this; + } + + + public AnalyticsBuilderCompat withShutdownHook(ShutdownHook shutdownHook) { + this.shutdownHook = (analytics) -> { + shutdownHook.onShutdown(analytics); + return Unit.INSTANCE; + }; + return this; + } + + public AnalyticsBuilderCompat withInitializationListener(InitializationListener initializationListener) { + this.initializationListener = (success, message) -> { + initializationListener.onInitialized(success, message); + return Unit.INSTANCE; + }; + return this; + } + public AnalyticsBuilderCompat withStorage(Storage storage) { + this.storage = storage; + return this; + } + + public Analytics build() { + return new Analytics(writeKey, configuration, + dataUploadService == null ? new DataUploadServiceImpl( + writeKey) : dataUploadService, configDownloadService == null ? + new ConfigDownloadServiceImpl(writeKey) : configDownloadService, + storage, + initializationListener, shutdownHook + ); + } + + @FunctionalInterface + public interface ShutdownHook { + void onShutdown(Analytics analytics); + } + + @FunctionalInterface + public interface InitializationListener { + void onInitialized(boolean success, String message); + } +} diff --git a/core/src/main/java/com/rudderstack/core/compat/BaseDestinationPluginCompat.java b/core/src/main/java/com/rudderstack/core/compat/BaseDestinationPluginCompat.java new file mode 100644 index 000000000..9f5a0dfe7 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/compat/BaseDestinationPluginCompat.java @@ -0,0 +1,88 @@ +package com.rudderstack.core.compat; + +import com.rudderstack.core.Analytics; +import com.rudderstack.core.BaseDestinationPlugin; +import com.rudderstack.core.Configuration; +import com.rudderstack.core.models.Message; +import com.rudderstack.core.models.RudderServerConfig; + +import org.jetbrains.annotations.NotNull; + +public abstract class BaseDestinationPluginCompat extends BaseDestinationPlugin { + protected BaseDestinationPluginCompat(@NotNull String name) { + super(name); + } + + @NotNull + @Override + public Message intercept(@NotNull Chain chain) { + return chain.proceed(chain.message()); + } + + @Override + public void setup(@NotNull Analytics analytics) { + super.setup(analytics); + } + + @Override + public void updateConfiguration(@NotNull Configuration configuration) { + super.updateConfiguration(configuration); + } + + @Override + public void updateRudderServerConfig(@NotNull RudderServerConfig config) { + super.updateRudderServerConfig(config); + } + + @Override + public void onShutDown() { + super.onShutDown(); + } + + @Override + public void reset() { + super.reset(); + } + + + + public static class DestinationInterceptorCompat implements DestinationInterceptor { + + @NotNull + @Override + public Message intercept(@NotNull Chain chain) { + return chain.proceed(chain.message()); + } + + @Override + public void setup(@NotNull Analytics analytics) { + } + + @Override + public void updateConfiguration(@NotNull Configuration configuration) { + } + + @Override + public void updateRudderServerConfig(@NotNull RudderServerConfig config) { + } + + @Override + public void onShutDown() { + } + + @Override + public void reset() { + } + + @NotNull + @Override + public Analytics getAnalytics() { + return null; //we will figure this out later + } + + @Override + public void setAnalytics(@NotNull Analytics analytics) { + + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/compat/ConfigurationBuilder.java b/core/src/main/java/com/rudderstack/core/compat/ConfigurationBuilder.java new file mode 100644 index 000000000..55a0e5bb8 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/compat/ConfigurationBuilder.java @@ -0,0 +1,103 @@ +package com.rudderstack.core.compat; + +import static com.rudderstack.core.Configuration.CONTROL_PLANE_URL; +import static com.rudderstack.core.Configuration.DATA_PLANE_URL; +import static com.rudderstack.core.Configuration.FLUSH_QUEUE_SIZE; +import static com.rudderstack.core.Configuration.MAX_FLUSH_INTERVAL; + +import com.rudderstack.core.Base64Generator; +import com.rudderstack.core.Configuration; +import com.rudderstack.core.Logger; +import com.rudderstack.core.RetryStrategy; +import com.rudderstack.core.RudderOption; +import com.rudderstack.core.RudderUtils; +import com.rudderstack.core.internal.KotlinLogger; +import com.rudderstack.rudderjsonadapter.JsonAdapter; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ConfigurationBuilder { + protected JsonAdapter jsonAdapter; + private RudderOption options = new RudderOption(); + private int flushQueueSize = FLUSH_QUEUE_SIZE; + private long maxFlushInterval = MAX_FLUSH_INTERVAL; + private boolean shouldVerifySdk = false; + private boolean gzipEnabled = true; + private RetryStrategy sdkVerifyRetryStrategy = RetryStrategy.exponential(); + private String dataPlaneUrl = DATA_PLANE_URL; + private String controlPlaneUrl = CONTROL_PLANE_URL; + private Logger logger = new KotlinLogger(); + private ExecutorService analyticsExecutor = Executors.newSingleThreadExecutor(); + private ExecutorService networkExecutor = Executors.newCachedThreadPool(); + private Base64Generator base64Generator = RudderUtils.INSTANCE.getDefaultBase64Generator(); + + public ConfigurationBuilder(JsonAdapter jsonAdapter) { + this.jsonAdapter = jsonAdapter; + } + + public ConfigurationBuilder withOptions(RudderOption options) { + this.options = options; + return this; + } + //builder for Configuration + + public ConfigurationBuilder withFlushQueueSize(int flushQueueSize) { + this.flushQueueSize = flushQueueSize; + return this; + } + + public ConfigurationBuilder withMaxFlushInterval(long maxFlushInterval) { + this.maxFlushInterval = maxFlushInterval; + return this; + } + + public ConfigurationBuilder withShouldVerifySdk(boolean shouldVerifySdk) { + this.shouldVerifySdk = shouldVerifySdk; + return this; + } + + public ConfigurationBuilder withGzipEnabled(boolean gzipEnabled) { + this.gzipEnabled = gzipEnabled; + return this; + } + + public ConfigurationBuilder withSdkVerifyRetryStrategy(RetryStrategy sdkVerifyRetryStrategy) { + this.sdkVerifyRetryStrategy = sdkVerifyRetryStrategy; + return this; + } + + public ConfigurationBuilder withDataPlaneUrl(String dataPlaneUrl) { + this.dataPlaneUrl = dataPlaneUrl; + return this; + } + + public ConfigurationBuilder withControlPlaneUrl(String controlPlaneUrl) { + this.controlPlaneUrl = controlPlaneUrl; + return this; + } + + public ConfigurationBuilder withLogLevel(Logger.LogLevel logLevel) { + this.logger = new KotlinLogger(logLevel); + return this; + } + + public ConfigurationBuilder withAnalyticsExecutor(ExecutorService analyticsExecutor) { + this.analyticsExecutor = analyticsExecutor; + return this; + } + + public ConfigurationBuilder withNetworkExecutor(ExecutorService networkExecutor) { + this.networkExecutor = networkExecutor; + return this; + } + + public ConfigurationBuilder withBase64Generator(Base64Generator base64Generator) { + this.base64Generator = base64Generator; + return this; + } + + public Configuration build() { + return new Configuration(jsonAdapter, options, flushQueueSize, maxFlushInterval, shouldVerifySdk, gzipEnabled, sdkVerifyRetryStrategy, dataPlaneUrl, controlPlaneUrl, logger, analyticsExecutor, networkExecutor, base64Generator); + } +} diff --git a/core/src/main/java/com/rudderstack/core/compat/PluginCompat.java b/core/src/main/java/com/rudderstack/core/compat/PluginCompat.java new file mode 100644 index 000000000..5065cde07 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/compat/PluginCompat.java @@ -0,0 +1,38 @@ +package com.rudderstack.core.compat; + +import com.rudderstack.core.Analytics; +import com.rudderstack.core.Configuration; +import com.rudderstack.core.Plugin; +import com.rudderstack.core.models.Message; +import com.rudderstack.core.models.RudderServerConfig; + +import org.jetbrains.annotations.NotNull; + +public abstract class PluginCompat implements Plugin { + @NotNull + @Override + public Message intercept(@NotNull Chain chain){ + return chain.proceed(chain.message()); + } + + @Override + public void setup(@NotNull Analytics analytics) { + } + + @Override + public void updateConfiguration(@NotNull Configuration configuration) { + } + + @Override + public void updateRudderServerConfig(@NotNull RudderServerConfig config) { + } + + @Override + public void onShutDown() { + } + + @Override + public void reset() { + } + +} diff --git a/core/src/main/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicy.kt b/core/src/main/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicy.kt new file mode 100644 index 000000000..626ae9317 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicy.kt @@ -0,0 +1,79 @@ +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +class CountBasedFlushPolicy : FlushPolicy { + + override lateinit var analytics: Analytics + + private var storage: Storage? = null + set(value) { + synchronized(this) { + field = value + } + } + get() = synchronized(this) { + field + } + private var flushCall: AtomicReference Unit> = AtomicReference({}) + private val flushQSizeThreshold = AtomicInteger(-1) + private val _isShutDown = AtomicBoolean(false) + private val onDataChange = object : Storage.DataListener { + override fun onDataChange() { + if (_isShutDown.get()) return + flushIfNeeded() + } + override fun onDataDropped(messages: List, error: Throwable) { + /** + * We won't be considering dropped events here + */ + } + } + + override fun setup(analytics: Analytics) { + super.setup(analytics) + if (storage == analytics.storage) return + storage = analytics.storage + storage?.addMessageDataListener(onDataChange) + } + + private fun flushIfNeeded() { + storage?.getCount { + val threshold = flushQSizeThreshold.get() + if (!_isShutDown.get() && threshold in 1..it) { + flushCall.get()(analytics) + } + } + } + + override fun reschedule() { + //-no-op + } + + override fun setFlush(flush: Analytics.() -> Unit) { + flushCall.set(flush) + } + + override fun onRemoved() { + storage?.removeMessageDataListener(onDataChange) + storage = null + } + + override fun updateConfiguration(configuration: Configuration) { + if (configuration.flushQueueSize == flushQSizeThreshold.get()) + return + flushQSizeThreshold.set(configuration.flushQueueSize) + flushIfNeeded() + } + + override fun shutdown() { + onRemoved() + _isShutDown.set(true) + } +} diff --git a/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPoliciesExtensions.kt b/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPoliciesExtensions.kt new file mode 100644 index 000000000..48887f2e5 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPoliciesExtensions.kt @@ -0,0 +1,51 @@ +/* + * Creator: Debanjan Chatterjee on 30/01/24, 6:48 pm Last modified: 30/01/24, 6:48 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Controller + +fun Controller.addFlushPolicies(vararg flushPolicies: FlushPolicy) { + addInfrastructurePlugin(*flushPolicies) + flushPolicies.forEach { + it.setFlush { flush() } + } +} +fun Controller.setFlushPolicies(vararg flushPolicies: FlushPolicy) { + synchronized(this) { + removeAllFlushPolicies() + addFlushPolicies(*flushPolicies) + } +} +fun Controller.removeAllFlushPolicies() { + val toBeRemoved = mutableListOf() + applyInfrastructureClosure{ + if(this is FlushPolicy) { + toBeRemoved += this + } + } + toBeRemoved.forEach { removeFlushPolicy(it) } +} +fun Controller.removeFlushPolicy(flushPolicy: FlushPolicy) { + flushPolicy.setFlush { } + flushPolicy.onRemoved() + removeInfrastructurePlugin(flushPolicy) +} + +fun Controller.applyFlushPoliciesClosure(closure : FlushPolicy.() -> Unit){ + applyInfrastructureClosure { + if (this is FlushPolicy) + this.closure() + } +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPolicy.kt b/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPolicy.kt new file mode 100644 index 000000000..b39d74f18 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/flushpolicy/FlushPolicy.kt @@ -0,0 +1,24 @@ +/* + * Creator: Debanjan Chatterjee on 31/01/24, 12:09 pm Last modified: 31/01/24, 9:49 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin + +interface FlushPolicy : InfrastructurePlugin { + fun reschedule() + fun onRemoved() + fun setFlush(flush: Analytics.() -> Unit) +} diff --git a/core/src/main/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicy.kt b/core/src/main/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicy.kt new file mode 100644 index 000000000..2dd095941 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicy.kt @@ -0,0 +1,107 @@ +/* + * Creator: Debanjan Chatterjee on 31/01/24, 2:51 pm Last modified: 31/01/24, 2:51 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import java.util.Timer +import java.util.TimerTask +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference + +class IntervalBasedFlushPolicy : FlushPolicy { + + override lateinit var analytics: Analytics + + private var thresholdCountDownTimer: Timer? = null + + private var flushCall: AtomicReference Unit> = AtomicReference({}) + + private val _isShutDown = AtomicBoolean(false) + private var _currentFlushIntervalAtomic = AtomicLong(0L) + private val _currentFlushInterval + get() = _currentFlushIntervalAtomic.get() + private var periodicTaskScheduler: TimerTask? = null + + override fun setup(analytics: Analytics) { + super.setup(analytics) + _isShutDown.set(false) + thresholdCountDownTimer = Timer(analytics.writeKey + "-IntervalBasedFlushPolicy") + + } + + override fun reschedule() { + if (_isShutDown.get()) return + rescheduleTimer() + } + + override fun onRemoved() { + periodicTaskScheduler?.cancel() + thresholdCountDownTimer?.purge() + periodicTaskScheduler = null + thresholdCountDownTimer = null + } + + override fun setFlush(flush: Analytics.() -> Unit) { + flushCall.set(flush) + } + + override fun updateConfiguration(configuration: Configuration) { + if (shouldRescheduleTimer(configuration)) { + updateMaxFlush(configuration.maxFlushInterval) + rescheduleTimer() + } + } + + private fun updateMaxFlush(maxFlushInterval: Long) { + _currentFlushIntervalAtomic.set(maxFlushInterval) + } + + private fun shouldRescheduleTimer(configuration: Configuration?): Boolean { + val newValue = configuration?.maxFlushInterval?.coerceAtLeast(0L) ?: 0L + return (!_isShutDown.get() && configuration != null && _currentFlushInterval != newValue) + } + + private fun rescheduleTimer() { + periodicTaskScheduler?.cancel() + thresholdCountDownTimer?.purge() + + if (_isShutDown.get()) { + return + } + + periodicTaskScheduler = createFlushTimerTask() + thresholdCountDownTimer?.schedule( + periodicTaskScheduler, _currentFlushInterval, _currentFlushInterval + ) + + } + + private fun createFlushTimerTask() = object : TimerTask() { + override fun run() { + synchronized(this@IntervalBasedFlushPolicy) { + if (!_isShutDown.get()) + flushCall.get()(analytics) + } + } + } + + override fun shutdown() { + if (_isShutDown.compareAndSet(false, true)) { + onRemoved() + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/holder/AnalyticsHolder.kt b/core/src/main/java/com/rudderstack/core/holder/AnalyticsHolder.kt new file mode 100644 index 000000000..264c138b8 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/holder/AnalyticsHolder.kt @@ -0,0 +1,43 @@ +/* + * Creator: Debanjan Chatterjee on 24/01/24, 11:39 am Last modified: 24/01/24, 11:35 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +@file:JvmName("AnalyticsHolder") +package com.rudderstack.core.holder + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Controller +import java.util.concurrent.ConcurrentHashMap + +/** + * A thread safe map to store the analytics data + * This data is not persisted and is mainly intended to maintain transient data associated to each analytics instance + */ +private val store = ConcurrentHashMap>() +fun Controller.store(identifier : String, value: Any){ + val analyticsStorageMap = store[this.writeKey] ?: ConcurrentHashMap().also { + store[writeKey] = it + } + analyticsStorageMap[identifier] = value +} +fun Controller.remove(identifier : String){ + val analyticsStorageMap = store[this.writeKey] ?: return + analyticsStorageMap.remove(identifier) +} +fun Controller.retrieve(identifier: String) : T?{ + val analyticsStorageMap = store[this.writeKey] ?: return null + return analyticsStorageMap[identifier] as? T +} + +fun Controller.clearAll(){ + store.remove(this.writeKey) +} diff --git a/core/src/main/java/com/rudderstack/core/holder/StatesHolder.kt b/core/src/main/java/com/rudderstack/core/holder/StatesHolder.kt new file mode 100644 index 000000000..cb5e6afb1 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/holder/StatesHolder.kt @@ -0,0 +1,41 @@ +/* + * Creator: Debanjan Chatterjee on 24/01/24, 11:39 am Last modified: 24/01/24, 11:39 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.holder +import com.rudderstack.core.State +import com.rudderstack.core.Analytics +import com.rudderstack.core.Controller + +/** + * This data is not persisted and is mainly intended to maintain transient State objects + * associated to each analytics instance + * Do not associate anonymous objects, as they will not be retrievable + */ + +/** + * [State] objects are associated to their Class names, hence the only one instance of a particular + * [State] can be associated to an [Analytics] instance. + * + * @param state + */ +fun Controller.associateState(state: State<*>){ + store(state.javaClass.name, state) +} +inline fun > Controller.removeState(){ + remove(T::class.java.name) +} + +inline fun > Controller.retrieveState() : T?{ + return retrieve(T::class.java.name) +} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/internal/AnalyticsDelegate.kt b/core/src/main/java/com/rudderstack/core/internal/AnalyticsDelegate.kt new file mode 100644 index 000000000..bd2858c8d --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/AnalyticsDelegate.kt @@ -0,0 +1,705 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Callback +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.Controller +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.DestinationConfig +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.LifecycleController +import com.rudderstack.core.Logger +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderOption +import com.rudderstack.core.RudderUtils.MAX_BATCH_SIZE +import com.rudderstack.core.RudderUtils.getUTF8Length +import com.rudderstack.core.Storage +import com.rudderstack.core.flushpolicy.CountBasedFlushPolicy +import com.rudderstack.core.flushpolicy.IntervalBasedFlushPolicy +import com.rudderstack.core.flushpolicy.addFlushPolicies +import com.rudderstack.core.flushpolicy.applyFlushPoliciesClosure +import com.rudderstack.core.holder.associateState +import com.rudderstack.core.holder.removeState +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.internal.plugins.CoreInputsPlugin +import com.rudderstack.core.internal.plugins.DestinationConfigurationPlugin +import com.rudderstack.core.internal.plugins.EventFilteringPlugin +import com.rudderstack.core.internal.plugins.EventSizeFilterPlugin +import com.rudderstack.core.internal.plugins.GDPRPlugin +import com.rudderstack.core.internal.plugins.RudderOptionPlugin +import com.rudderstack.core.internal.plugins.StoragePlugin +import com.rudderstack.core.internal.plugins.WakeupActionPlugin +import com.rudderstack.core.internal.states.ConfigurationsState +import com.rudderstack.core.internal.states.DestinationConfigState +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import com.rudderstack.web.HttpResponse +import java.util.concurrent.ExecutorService +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.RejectedExecutionHandler +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +internal class AnalyticsDelegate( + configuration: Configuration, + override val storage: Storage, + override val writeKey: String, + override val dataUploadService: DataUploadService, + override val configDownloadService: ConfigDownloadService?, + //optional + private val initializationListener: ((success: Boolean, message: String?) -> Unit)? = null, + //optional called if shutdown is called + private val shutdownHook: (Analytics.() -> Unit)? = null +) : Controller { + + companion object { + + //the number of flush calls that can be queued + //unit sized queue means only one flush call can wait for the ongoing to complete + private const val NUMBER_OF_FLUSH_CALLS_IN_QUEUE = 2 + private val PLUGIN_LOCK = Any() + } + + private val _isShutDown = AtomicBoolean(false) + + //keep track of Analytics object + private var _analytics: Analytics? = null + + /** + * A handler for rejected tasks that discards the oldest unhandled request and then retries + * execute, unless the executor is shut down, in which case the task is discarded. + */ + private val handler: RejectedExecutionHandler = ThreadPoolExecutor.DiscardOldestPolicy() + + //used for flushing + // a single threaded executor by default for sequentially calling flush one after other + private val _flushExecutor = ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + LinkedBlockingQueue(NUMBER_OF_FLUSH_CALLS_IN_QUEUE), + handler + ) + + +// private val _commonContext = mapOf( +// "library" to (configuration.jsonAdapter.writeToJson(mapOf( +// "name" to configuration.storage.libraryName, +// "version" to configuration.storage.libraryVersion +// ), +// object : RudderTypeAdapter>() {}) ?: "") +// +// ) + + override val isShutdown + get() = _isShutDown.get() + override val logger: Logger + get() = currentConfiguration?.logger ?: Logger.Noob + + override fun clearStorage() { + storage.clearStorage() + } + + override fun reset() { + applyInfrastructureClosure { + this.reset() + } + applyMessageClosure { + this.reset() + } + } + + //message callbacks + private var _callbacks = setOf() + + private var _destinationPlugins: List> = mutableListOf() + + //added before local message plugins + private var _internalPreMessagePlugins: List = mutableListOf() + + + private var _customPlugins: List = mutableListOf() + + private var _infrastructurePlugins: List = mutableListOf() + + //added after custom plugins + private var _internalPostCustomPlugins: List = mutableListOf() + + //Timeline plugins are associated throughout the lifecycle of SDK. + private val _allTimelinePlugins + get() = _internalPreMessagePlugins + _customPlugins + _internalPostCustomPlugins + _destinationPlugins + + + //plugins + private val gdprPlugin = GDPRPlugin() + private val eventSizeFilterPlugin = EventSizeFilterPlugin() + private val storagePlugin = StoragePlugin() + private val wakeupActionPlugin = WakeupActionPlugin() + private val eventFilteringPlugin = EventFilteringPlugin() + private val coreInputsPlugin = CoreInputsPlugin() +// destConfigState = DestinationConfigState + + private val destinationConfigurationPlugin = DestinationConfigurationPlugin() + + private var _serverConfig: RudderServerConfig? = null + + init { + associateState(ConfigurationsState(configuration)) + associateState(DestinationConfigState()) + attachListeners() + initializePlugins() + initializeFlush() + } + + private fun attachListeners() { + currentConfigurationState?.subscribe { state, _ -> + logger.debug(log = "Configuration updated: $state") + applyInfrastructureClosure { + applyConfigurationClosure(this) + } + applyMessageClosure { + applyConfigurationClosure(this) + } + } + } + + private fun initializeFlush() { + //these are the defaults + addFlushPolicies(CountBasedFlushPolicy(), IntervalBasedFlushPolicy()) + } + + private fun initializePlugins() { + initializeInfraPlugins() + initializeMessagePlugins() + } + + private fun initializeInfraPlugins() { + addInfrastructurePlugin(storage) + configDownloadService?.let { + addInfrastructurePlugin(it) + } ?: logger.warn(log = "ConfigDownloadService not set") + addInfrastructurePlugin(dataUploadService) + } + + + override fun applyConfiguration(configurationScope: Configuration.() -> Configuration) { + currentConfiguration?.let { + val newConfiguration = configurationScope(it) + currentConfigurationState?.update(newConfiguration) + + } ?: logger.error(log = "Configuration not updated, since current configuration is null") + + } + + override fun applyMessageClosure(closure: Plugin.() -> Unit) { + synchronized(PLUGIN_LOCK) { + _allTimelinePlugins.forEach { + it.closure() + } + } + } + + override fun applyInfrastructureClosure(closure: InfrastructurePlugin.() -> Unit) { + synchronized(PLUGIN_LOCK) { + _infrastructurePlugins.forEach { + it.closure() + } + } + } + + override fun optOut(optOut: Boolean) { + storage.saveOptOut(optOut) + } + + override val isOptedOut: Boolean + get() = storage.isOptedOut + private val currentConfigurationState: ConfigurationsState? + get() = retrieveState() + private val currentDestinationConfigurationState: DestinationConfigState? + get() = retrieveState().also { + if (it == null) logger.error(log = "DestinationConfigState state not found") + } + override val currentConfiguration: Configuration? + get() = currentConfigurationState?.value + + + override fun addPlugin(vararg plugins: Plugin) { + synchronized(PLUGIN_LOCK) { + if (plugins.isEmpty()) return + plugins.forEach { + if (it is DestinationPlugin<*>) { + _destinationPlugins += it + val newDestinationConfig = currentDestinationConfigurationState?.value?.withIntegration( + it.name, it.isReady + ) ?: DestinationConfig(mapOf(it.name to it.isReady)) + currentDestinationConfigurationState?.update(newDestinationConfig) + initDestinationPlugin(it) + } else _customPlugins = _customPlugins + it + + //startup + _analytics?.apply { + _analytics?.logger?.debug(log = "========= setting plugin up $it =========") + Logger.LogLevel.DEBUG + it.setup(this) + } + applyUpdateClosures(it) + } + } + + } + + override fun removePlugin(plugin: Plugin): Boolean { + if (plugin is DestinationPlugin<*>) synchronized(PLUGIN_LOCK) { + val destinationPluginPrevSize = _destinationPlugins.size + _destinationPlugins = _destinationPlugins - plugin + return _destinationPlugins.size < destinationPluginPrevSize + } + synchronized(PLUGIN_LOCK) { + val customPluginPrevSize = _customPlugins.size + _customPlugins = _customPlugins - plugin + return (_customPlugins.size < customPluginPrevSize) + } + + } + + override fun addInfrastructurePlugin(vararg plugins: InfrastructurePlugin) { + val filteredPlugins = filterAcceptablePlugins(plugins) + synchronized(PLUGIN_LOCK) { + _infrastructurePlugins += filteredPlugins + } + filteredPlugins.forEach { plugin -> + _analytics?.let { + plugin.setup(it) + applyUpdateClosures(plugin) + } + } + } + + private fun filterAcceptablePlugins(plugins: Array): List { + val isConfigDownloadServiceAlreadySet = + _infrastructurePlugins.any { it is ConfigDownloadService } + if (!isConfigDownloadServiceAlreadySet) return plugins.toList() + return plugins.filter { + when (it) { + is ConfigDownloadService -> false.also { + currentConfiguration?.logger?.warn( + log = "ConfigDownloadService already set. Dropping plugin $it" + ) + } + + else -> true + } + } + } + + override fun removeInfrastructurePlugin(plugin: InfrastructurePlugin): Boolean { + val prevSize = _infrastructurePlugins.size + synchronized(PLUGIN_LOCK) { + _infrastructurePlugins -= plugin + } + return _infrastructurePlugins.size < prevSize + } + + + override fun processMessage( + message: Message, options: RudderOption?, lifecycleController: LifecycleController? + ) { + if (isShutdown) { + logger.warn(log = "Analytics has shut down, ignoring message $message") + return + } + currentConfiguration?.analyticsExecutor?.execute { + val lcc = lifecycleController ?: LifecycleControllerImpl( + message, generatePluginsWithOptions(options) + ) + lcc.process() + + } + } + + private fun generatePluginsWithOptions(options: RudderOption?): List { + return synchronized(PLUGIN_LOCK) { + _internalPreMessagePlugins + (options ?: currentConfiguration?.options + ?: RudderOption()).createPlugin() + _customPlugins + _internalPostCustomPlugins + _destinationPlugins + }.toList() + } + + private fun RudderOption.createPlugin(): Plugin { + return RudderOptionPlugin( + this + ).also { + it.setup(analytics = _analytics ?: return@also) + } + } + + override fun addCallback(callback: Callback) { + _callbacks = _callbacks + callback + } + + override fun removeCallback(callback: Callback) { + _callbacks = _callbacks - callback + } + + override fun removeAllCallbacks() { + _callbacks = setOf() + } + + override fun flush() { + if (isShutdown) return + currentConfiguration?.let { + forceFlush(_flushExecutor) + } + } + // works even after shutdown + + private fun forceFlush( + flushExecutor: ExecutorService, callback: ((Boolean) -> Unit)? = null + ) { + flushExecutor.submit { + blockingFlush().let { + callback?.invoke(it) + } + } + } + + private val _isFlushing = AtomicBoolean(false) + override fun blockingFlush( + ): Boolean { + if (_isShutDown.get()) return false + if (!_isFlushing.compareAndSet(false, true)) return false + //inform plugins + broadcastFlush() + + // Add `extraInfo` size if sent. Default batch size is 2KB. + var latestData = getMessagesTrimmedToMaxSize() + + var isFlushSuccess = true + + while (latestData.isNotEmpty()) { + applyInfrastructureClosure { + if (this is DataUploadService) { + val response = uploadSync(latestData, null) + if (response == null) { + isFlushSuccess = false + return@applyInfrastructureClosure + } + if (response.success) { + latestData.successCallback() + storage.deleteMessagesSync(latestData) + // Add `extraInfo` size if sent. Default batch size is 2KB. + latestData = getMessagesTrimmedToMaxSize() + } else { + latestData.failureCallback(response.errorThrowable) + isFlushSuccess = false + } + } + } + } + _isFlushing.set(false) + return isFlushSuccess + } + + private fun getMessagesTrimmedToMaxSize(defaultBufferSize: Int = 2 * 1024): List { + val data = storage.getDataSync() + val config = currentConfiguration ?: return data + + var totalMessageSize = defaultBufferSize + var index = 0 + + for (message in data) { + val messageJSON: String? = config.jsonAdapter.writeToJson(message, object : RudderTypeAdapter() {}) + val messageSize = messageJSON.toString().getUTF8Length() + + totalMessageSize += messageSize + + if (totalMessageSize > MAX_BATCH_SIZE) { + config.logger.debug(log = "Maximum batch size reached at $index") + break + } + index++ + } + + return data.subList(0, index) + } + + private fun broadcastFlush() { + applyMessageClosure { + if (this is DestinationPlugin<*>) this.flush() + } + applyFlushPoliciesClosure { + this.reschedule() + } + } + + override fun shutdown() { + if (!_isShutDown.compareAndSet(false, true)) return + logger.info(log = "shutdown") + //inform plugins + shutdownPlugins() + + storage.shutdown() + currentConfiguration?.analyticsExecutor?.shutdownNow() + + _flushExecutor.shutdownNow() + removeState() + removeState() + println("shutting down analytics $_analytics") + shutdownHook?.invoke(_analytics ?: return) + } + + private fun shutdownPlugins() { + applyMessageClosure { + onShutDown() + } + applyInfrastructureClosure { + shutdown() + } + } + + + private fun initDestinationPlugin(plugin: DestinationPlugin<*>) { + + val destConfig = currentDestinationConfigurationState?.value ?: DestinationConfig() + + if (!destConfig.isIntegrationReady(plugin.name)) { + + plugin.addIsReadyCallback { _, isReady -> + onDestinationReady(isReady, plugin) + } + } else { + //destination is ready for startup queue + onDestinationReady(true, plugin) + } + } + + private fun onDestinationReady(isReady: Boolean, plugin: DestinationPlugin<*>) { + if (isReady) { + storage.startupQueue?.forEach { + // will be sent only for the individual destination. + //options need not be considered, since RudderOptionPlugin has + //already done it's job. + processMessage( + message = it, + null, + lifecycleController = LifecycleControllerImpl(it, listOf(plugin)) + ) + } + val newDestinationConfig = (currentDestinationConfigurationState?.value + ?: DestinationConfig()).withIntegration( + plugin.name, isReady + ) + currentDestinationConfigurationState?.update(newDestinationConfig) + if (newDestinationConfig.allIntegrationsReady) { + //all integrations are ready, time to clear startup queue + storage.clearStartupQueue() + } + } else { + logger.warn(log = "plugin ${plugin.name} activation failed") + //remove from destination config, else all integrations ready won't be true anytime + + val newDestinationConfig = (currentDestinationConfigurationState?.value + ?: DestinationConfig()).removeIntegration(plugin.name) + currentDestinationConfigurationState?.update(newDestinationConfig) + + } + } + override fun updateSourceConfig() { + var isServerConfigDownloadPossible = false + applyInfrastructureClosure { + if (this is ConfigDownloadService) { + currentConfiguration?.apply { + downloadServerConfig( + this@applyInfrastructureClosure, this + ) + isServerConfigDownloadPossible = true + } + } + } + if (!isServerConfigDownloadPossible) { + initializationListener?.invoke( + false, "Config download service not set or " + "configuration not available" + ) + logger.error(log = "Config Download Service Not Set or Configuration not available") + shutdown() + return + } + } + + private fun downloadServerConfig( + configDownloadService: ConfigDownloadService, configuration: Configuration + ) { + configDownloadService.download { success, rudderServerConfig, lastErrorMsg -> + configuration.analyticsExecutor.submit { + if (success && rudderServerConfig != null && rudderServerConfig.source?.isSourceEnabled != false) { + initializationListener?.invoke(true, null) + handleConfigData(rudderServerConfig) + } else { + val cachedConfig = storage.serverConfig + if (cachedConfig != null) { + initializationListener?.invoke( + true, "Downloading failed, using cached context" + ) + logger.warn(log = "Downloading failed, using cached context") + handleConfigData(cachedConfig) + } else { + logger.error(log = "SDK Initialization failed due to $lastErrorMsg") + initializationListener?.invoke( + false, "Downloading failed, Shutting down $lastErrorMsg" + ) + //log lastErrorMsg or isSourceEnabled + shutdown() + } + } + } + } + } + + private fun handleConfigData(serverConfig: RudderServerConfig) { + storage.saveServerConfig(serverConfig) + _serverConfig = serverConfig + + applyInfrastructureClosure { + applyServerConfigClosure(this) + } + applyMessageClosure { + applyServerConfigClosure(this) + } + + } + + /** + * pre destination plugins can be initialized before-hand + * + */ + private fun initializeMessagePlugins() { + // check if opted out + _internalPreMessagePlugins += gdprPlugin + _internalPreMessagePlugins += coreInputsPlugin + _internalPostCustomPlugins += eventSizeFilterPlugin + // rudder option plugin followed by extract state plugin should be added by lifecycle + // add defaults to message +// _internalPrePlugins = _internalPrePlugins + anonymousIdPlugin + + // store for cloud destinations +// _internalPostMessagePlugins = _internalPostMessagePlugins + fillDefaultsPlugin +// _internalPostMessagePlugins = _internalPostMessagePlugins + extractStatePlugin + + _internalPostCustomPlugins += destinationConfigurationPlugin + _internalPostCustomPlugins += wakeupActionPlugin + _internalPostCustomPlugins += eventFilteringPlugin + _internalPostCustomPlugins += storagePlugin + + } + + /** + * Any initialization that requires [Analytics] is to be done here + * + * @param analytics + */ + internal fun startup(analytics: Analytics) { + messagePluginStartupClosure(analytics) + infraPluginStartupClosure(analytics) + _analytics = analytics + if (currentConfiguration?.shouldVerifySdk == true) { + updateSourceConfig() + } else { + initializationListener?.invoke(true, null) + } + } + + private fun infraPluginStartupClosure(analytics: Analytics) { + applyInfrastructureClosure { + setup(analytics) + } + applyInfrastructureClosure { + applyUpdateClosures(this) + } + } + + private fun messagePluginStartupClosure(analytics: Analytics) { + //apply startup closure + applyMessageClosure { + setup(analytics) + } + //if plugin update related configs are ready + applyMessageClosure { + applyUpdateClosures(this) + } + } + + private fun applyUpdateClosures(plugin: Plugin) { + //apply setupClosures + //config closure + applyConfigurationClosure(plugin) + //server config closure, if available + applyServerConfigClosure(plugin) + + } + + private fun applyUpdateClosures(plugin: InfrastructurePlugin) { + //apply setupClosures + //config closure + applyConfigurationClosure(plugin) + //server config closure, if available + applyServerConfigClosure(plugin) + + } + + private fun applyConfigurationClosure(plugin: Plugin) { + currentConfiguration?.apply { + plugin.updateConfiguration(this) + } + } + + private fun applyConfigurationClosure(plugin: InfrastructurePlugin) { + currentConfiguration?.apply { + plugin.updateConfiguration(this) + } + } + + private fun applyServerConfigClosure(plugin: InfrastructurePlugin) { + _serverConfig?.apply { + plugin.updateRudderServerConfig(this@apply) + } + } + + private fun applyServerConfigClosure(plugin: Plugin) { + _serverConfig?.apply { + plugin.updateRudderServerConfig(this@apply) + } + } + + //extensions + private fun Iterable.successCallback() { + if (_callbacks.isEmpty()) return + forEach { + _callbacks.forEach { callback -> + callback.success(it) + } + } + } + + private fun Iterable.failureCallback(throwable: Throwable) { + if (_callbacks.isEmpty()) return + forEach { + _callbacks.forEach { callback -> + callback.failure(it, throwable) + } + } + } + + private val HttpResponse<*>.success: Boolean + get() = status in (200..209) + + private val HttpResponse<*>.errorThrowable: Throwable + get() = error ?: errorBody?.let { + Exception(it) + } ?: Exception("Internal error") + + +} diff --git a/core/src/main/java/com/rudderstack/core/internal/CentralPluginChain.kt b/core/src/main/java/com/rudderstack/core/internal/CentralPluginChain.kt new file mode 100644 index 000000000..e50d3f81b --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/CentralPluginChain.kt @@ -0,0 +1,61 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message +import java.util.concurrent.atomic.AtomicInteger + +/** + * A concrete plugin chain that carries the entire plugin chain: all application + * plugins, database plugins, and finally the cloud destination. + * + */ +internal class CentralPluginChain( + private val message: Message, + override val plugins: List, + override val index: Int = 0, + override val originalMessage: Message +) : Plugin.Chain { + private val numberOfCalls = AtomicInteger(0) + override fun message(): Message { + return message + } + + override fun proceed(message: Message): Message { + if (plugins.size <= index) + return message + // a chain can be proceeded just once + check(numberOfCalls.incrementAndGet() < 2) { + "proceed cannot be called on same chain twice" + } + // Call the next interceptor in the chain. + val plugin = plugins[index] + val next = if (plugin is DestinationPlugin<*>) { + // destination plugins will be getting a copy, so they don't tamper the original + val msgCopy = message.copy() + val subPlugins = plugin.subPlugins + val subPluginsModifiedCopyMsg = if (subPlugins.isNotEmpty()) { + val realSubPluginChain = copy(msgCopy, subPlugins, 0) + realSubPluginChain.proceed(msgCopy) + } else msgCopy // message specifically modified for a destination plugin + + //message is the altered message object and unaltered message is sent as original object + copy(index = index + 1, message = subPluginsModifiedCopyMsg, originalMessage = message) + } else + //in case of other plugins, change is propagated + copy(index = index + 1, message = message, originalMessage = originalMessage) + return plugin.intercept(next) + + } + + override fun with(plugins: List): Plugin.Chain { + return copy(plugins = plugins) + } + + internal fun copy( + message: Message = this.message, + plugins: List = this.plugins, + index: Int = this.index, + originalMessage: Message = this.originalMessage + ) = CentralPluginChain(message, plugins, index, originalMessage) +} diff --git a/core/src/main/java/com/rudderstack/core/internal/ConfigDownloadServiceImpl.kt b/core/src/main/java/com/rudderstack/core/internal/ConfigDownloadServiceImpl.kt new file mode 100644 index 000000000..0c01c2515 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/ConfigDownloadServiceImpl.kt @@ -0,0 +1,120 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.web.HttpResponse +import com.rudderstack.web.WebService +import com.rudderstack.web.WebServiceFactory +import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference + +internal class ConfigDownloadServiceImpl @JvmOverloads constructor( + private val writeKey: String, + webService: WebService? = null, +) : ConfigDownloadService { + + override lateinit var analytics: Analytics + + private val downloadSequence = CopyOnWriteArrayList() + private var listeners = CopyOnWriteArrayList() + private val controlPlaneWebService: AtomicReference = + AtomicReference(webService) + private val encodedWriteKey: AtomicReference = AtomicReference() + private val currentConfigurationAtomic = AtomicReference() + private val currentConfiguration + get() = currentConfigurationAtomic.get() + + private fun Configuration.initializeWebServiceIfRequired() { + if (controlPlaneWebService.get() == null) controlPlaneWebService.set( + WebServiceFactory.getWebService( + controlPlaneUrl, jsonAdapter = jsonAdapter, executor = networkExecutor + ) + ) + } + + private var ongoingConfigFuture: Future>? = null + private var lastRudderServerConfig: RudderServerConfig? = null + private var lastErrorMsg: String? = null + + override fun download( + callback: (success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit + ) { + currentConfiguration?.apply { + networkExecutor.submit { + with(sdkVerifyRetryStrategy) { + analytics.perform({ + ongoingConfigFuture = controlPlaneWebService.get()?.get( + mapOf( + "Content-Type" to "application/json", + "Authorization" to String.format( + Locale.US, + "Basic %s", + encodedWriteKey + ) + ), mapOf( + "p" to this.storage.libraryPlatform, + "v" to this.storage.libraryVersion, + "bv" to this.storage.libraryOsVersion + ), "sourceConfig", RudderServerConfig::class.java + ) + val response = ongoingConfigFuture?.get() + lastRudderServerConfig = response?.body + lastErrorMsg = response?.errorBody ?: response?.error?.message + return@perform (ongoingConfigFuture?.get()?.status ?: -1) == 200 //TODO - + // if the status is excluded from retry 429 or 500-599 + }) { + listeners.forEach { listener -> + listener.onDownloaded(it) + } + downloadSequence.add(it) + callback.invoke(it, lastRudderServerConfig, lastErrorMsg) + } + } + } + } + + } + + override fun addListener(listener: ConfigDownloadService.Listener, replay: Int) { + val replayCount = replay.coerceAtLeast(0) + replayConfigDownloadHistory(replayCount, listener) + listeners += listener + } + + private fun replayConfigDownloadHistory( + replayCount: Int, + listener: ConfigDownloadService.Listener + ) { + for (i in (downloadSequence.size - replayCount).coerceAtLeast(0) until downloadSequence.size) { + listener.onDownloaded(downloadSequence[i]) + } + } + + override fun removeListener(listener: ConfigDownloadService.Listener) { + listeners.remove(listener) + } + + override fun updateConfiguration(configuration: Configuration) { + encodedWriteKey.set(configuration.base64Generator.generateBase64(writeKey)) + configuration.initializeWebServiceIfRequired() + currentConfigurationAtomic.set(configuration) + } + + override fun shutdown() { + listeners.clear() + downloadSequence.clear() + controlPlaneWebService.get()?.shutdown() + controlPlaneWebService.set(null) + currentConfigurationAtomic.set(null) + encodedWriteKey.set(null) + try { + ongoingConfigFuture?.cancel(true) + } catch (ex: Exception) { + // Ignore the exception + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/DataUploadServiceImpl.kt b/core/src/main/java/com/rudderstack/core/internal/DataUploadServiceImpl.kt new file mode 100644 index 000000000..4402ae743 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/DataUploadServiceImpl.kt @@ -0,0 +1,137 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.models.Message +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import com.rudderstack.web.HttpInterceptor +import com.rudderstack.web.HttpResponse +import com.rudderstack.web.WebService +import com.rudderstack.web.WebServiceFactory +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +internal class DataUploadServiceImpl @JvmOverloads constructor( + private val writeKey: String, + webService: WebService? = null, +) : DataUploadService { + + override lateinit var analytics: Analytics + + private val encodedWriteKey: AtomicReference = AtomicReference() + private val webService: AtomicReference = AtomicReference(webService) + private val currentConfigurationAtomic = AtomicReference() + private var headers = mutableMapOf() + + private val _isPaused = AtomicBoolean(false) + private val currentConfiguration + get() = currentConfigurationAtomic.get() + + private val interceptor = HttpInterceptor { + if (headers.isNotEmpty()) { + synchronized(this) { + headers.forEach { (key, value) -> + it.setRequestProperty(key, value) + } + } + } + it + } + + private fun Configuration.initializeWebService() { + if (webService.get() == null) webService.set( + WebServiceFactory.getWebService( + dataPlaneUrl, jsonAdapter, executor = networkExecutor + ).also { + it.setInterceptor(interceptor) + } + ) + } + + override fun addHeaders(headers: Map) { + synchronized(this) { + this.headers += headers + } + } + + // private val configurationState: State = + override fun upload( + data: List, + extraInfo: Map?, + callback: (response: HttpResponse) -> Unit + ) { + if (_isPaused.get()) { + callback(HttpResponse(-1, null, "Upload Service is Paused", null)) + return + } + + currentConfiguration?.apply { + if (networkExecutor.isShutdown || networkExecutor.isTerminated) return + val batchBody = createBatchBody(data, extraInfo) + webService.get()?.post( + mapOf( + "Content-Type" to "application/json", + "Authorization" to String.format(Locale.US, "Basic %s", encodedWriteKey), + ), null, batchBody, "v1/batch", String::class.java, gzipEnabled + ) { + callback.invoke(it) + } + } + } + + + override fun uploadSync(data: List, extraInfo: Map?): HttpResponse? { + if (_isPaused.get()) return null + return currentConfiguration?.let { config -> + val batchBody = config.createBatchBody(data, extraInfo) + webService.get()?.post( + mapOf( + "Content-Type" to "application/json", + "Authorization" to String.format(Locale.US, "Basic %s", encodedWriteKey) + ), null, batchBody, "v1/batch", + String::class.java, isGzipEnabled = config.gzipEnabled + )?.get() + } + } + + private fun Configuration.createBatchBody( + data: List, + extraInfo: Map? + ): String? { + val sentAt = RudderUtils.timeStamp + return mapOf( + "sentAt" to sentAt, "batch" to data.map { + it.sentAt = sentAt + it + } + ).let { + if (extraInfo.isNullOrEmpty()) it else it + extraInfo //adding extra info data + }.let { + jsonAdapter.writeToJson(it, object : RudderTypeAdapter>() {}) + } + } + + override fun updateConfiguration(configuration: Configuration) { + encodedWriteKey.set(configuration.base64Generator.generateBase64(writeKey)) + configuration.initializeWebService() + currentConfigurationAtomic.set(configuration) + } + + override fun pause() { + _isPaused.set(true) + } + + override fun resume() { + _isPaused.set(false) + } + + override fun shutdown() { + webService.get()?.shutdown() + webService.set(null) + currentConfigurationAtomic.set(null) + encodedWriteKey.set(null) + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/KotlinLogger.kt b/core/src/main/java/com/rudderstack/core/internal/KotlinLogger.kt new file mode 100644 index 000000000..44a774f0c --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/KotlinLogger.kt @@ -0,0 +1,39 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Logger + +class KotlinLogger( + initialLogLevel: Logger.LogLevel = Logger.DEFAULT_LOG_LEVEL +) : Logger { + + private var logLevel = initialLogLevel + @Synchronized set + @Synchronized get + + override fun activate(level: Logger.LogLevel) { + logLevel = level + } + + override fun info(tag: String, log: String) { + if (Logger.LogLevel.INFO >= logLevel) + println("$tag-info : $log") + } + + override fun debug(tag: String, log: String) { + if (Logger.LogLevel.DEBUG >= logLevel) + println("$tag-debug : $log") + } + + override fun warn(tag: String, log: String) { + if (Logger.LogLevel.WARN >= logLevel) + println("$tag-warn : $log") + } + + override fun error(tag: String, log: String, throwable: Throwable?) { + if (Logger.LogLevel.ERROR >= logLevel) + println("$tag-error : $log") + } + + override val level: Logger.LogLevel + get() = logLevel +} diff --git a/core/src/main/java/com/rudderstack/core/internal/LifecycleControllerImpl.kt b/core/src/main/java/com/rudderstack/core/internal/LifecycleControllerImpl.kt new file mode 100644 index 000000000..54fb3b641 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/LifecycleControllerImpl.kt @@ -0,0 +1,21 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.LifecycleController +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message + +/** + * LCC implementation that processes a message through it's lifetime + * @see LifecycleController + * @property message The associated message + * @property plugins The plugins that will work on the Message. + */ +internal class LifecycleControllerImpl( + override val message: Message, + override val plugins: List +) : LifecycleController { + override fun process() { + val centralPluginChain = CentralPluginChain(message, plugins, originalMessage = message) + centralPluginChain.proceed(message) + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/CoreInputsPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/CoreInputsPlugin.kt new file mode 100644 index 000000000..8dc180d7a --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/CoreInputsPlugin.kt @@ -0,0 +1,27 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message + +private const val LIBRARY_KEY = "library" + +/** + * Plugin to add the library details to context object of payload. + */ +class CoreInputsPlugin : Plugin { + override lateinit var analytics: Analytics + + private val Storage.libraryContextPair + get() = LIBRARY_KEY to mapOf("name" to libraryName, "version" to libraryVersion) + + override fun intercept(chain: Plugin.Chain): Message { + val message = chain.message() + val context = analytics.storage.let { storage -> + message.context?.let { it + storage.libraryContextPair } ?: mapOf(storage.libraryContextPair) + } + return chain.proceed(message.copy(context = context)) + } + +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPlugin.kt new file mode 100644 index 000000000..7a8325453 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPlugin.kt @@ -0,0 +1,46 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig + +/** + * Enables or disables destination plugins based on server config. + * There can be three cases. + * In case there is no device mode plugins, there will be no checking for Destination configuration. + * + * In case device mode plugins are present but no destination configuration, destination plugins will + * be removed (though this scenario is very hard to produce, since as device mode plugins are required to + * setup when server config is available), else it is responsibility of other plugins to do the same. + * + * In case device mode destination plugins are present along with server config, all destination + * plugins that are disabled in destination config are filtered out. + */ +internal class DestinationConfigurationPlugin : Plugin { + override lateinit var analytics: Analytics + + private var _notAllowedDestinations: Set = setOf() + private var _isConfigUpdated = false + override fun intercept(chain: Plugin.Chain): Message { + val msg = chain.message() + val validPlugins = chain.plugins.filter { + //either not a destination plugin or is allowed + it !is DestinationPlugin<*> || (_isConfigUpdated && it.name !in _notAllowedDestinations) + } + return if (validPlugins.isNotEmpty()) { + return chain.with(validPlugins).proceed(msg) + } else + chain.proceed(msg) + } + + override fun updateRudderServerConfig(config: RudderServerConfig) { + _isConfigUpdated = true + _notAllowedDestinations = config.source?.destinations?.filter { + !it.isDestinationEnabled + }?.map { + it.destinationDefinition?.definitionName ?: it.destinationName ?: "" + }?.toHashSet() ?: setOf() + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/EventFilteringPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/EventFilteringPlugin.kt new file mode 100644 index 000000000..3e18c71e6 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/EventFilteringPlugin.kt @@ -0,0 +1,83 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import java.util.concurrent.ConcurrentHashMap + +/** + * Event filtering for device mode destinations. Filters out blacklisted or whitelisted events in + * source config + * + */ +private const val DISABLE = "disable" +private const val WHITELISTED_EVENTS = "whitelistedEvents" +private const val BLACKLISTED_EVENTS = "blacklistedEvents" +private const val EVENT_FILTERING_OPTION = "eventFilteringOption" +private const val EVENT_NAME = "eventName" + +class EventFilteringPlugin : Plugin { + + override lateinit var analytics: Analytics + + //map of (destination definition name, DestinationEventFilteringConfig) + private var filteredEventsMap = ConcurrentHashMap() + + override fun intercept(chain: Plugin.Chain): Message { + val msg = chain.message() + return if (filteredEventsMap.isEmpty()) { + chain.proceed(msg) + } else { + chain.with(chain.plugins.filterNot { + it is DestinationPlugin<*> && msg is TrackMessage && + filteredEventsMap[it.name]?.let { config -> + when (config.status) { + WHITELISTED_EVENTS -> !config.listedEvents.contains(msg.eventName) + BLACKLISTED_EVENTS -> config.listedEvents.contains(msg.eventName) + else -> false + } + } ?: false + }).proceed(msg) + } + } + + override fun updateRudderServerConfig(config: RudderServerConfig) { + super.updateRudderServerConfig(config) + config.source?.destinations?.apply { + cacheFilteredEvents(this) + } + } + + private fun cacheFilteredEvents(destinations: List) { + if (destinations.isNotEmpty()) { + // Iterate all destinations + for (destination in destinations) { + val destinationDefinitionName = + destination?.destinationDefinition?.definitionName ?: continue + val eventFilteringStatus = + destination.destinationConfig[EVENT_FILTERING_OPTION] as? String ?: DISABLE + val destinationFilterConfig = DestinationEventFilteringConfig(eventFilteringStatus, + eventFilteringStatus.takeIf { it != DISABLE } + ?.let { destination.destinationConfig[it] as? Collection>? } + ?.mapNotNull { + it[EVENT_NAME] + }?.toHashSet() ?: setOf() + ) + filteredEventsMap[destinationDefinitionName] = destinationFilterConfig + } + } + } + + override fun onShutDown() { + super.onShutDown() + filteredEventsMap.clear() + } + + private data class DestinationEventFilteringConfig( + val status: String, + val listedEvents: Set + ) +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/EventSizeFilterPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/EventSizeFilterPlugin.kt new file mode 100644 index 000000000..a49766af5 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/EventSizeFilterPlugin.kt @@ -0,0 +1,40 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils.MAX_EVENT_SIZE +import com.rudderstack.core.RudderUtils.getUTF8Length +import com.rudderstack.core.models.Message +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import java.util.concurrent.atomic.AtomicReference + +/** + * A plugin to filter out events that exceed the maximum size limit. + */ +class EventSizeFilterPlugin : Plugin { + + override lateinit var analytics: Analytics + + private val currentConfigurationAtomic = AtomicReference() + private val currentConfiguration + get() = currentConfigurationAtomic.get() + + override fun updateConfiguration(configuration: Configuration) { + currentConfigurationAtomic.set(configuration) + } + + override fun intercept(chain: Plugin.Chain): Message { + currentConfiguration?.let { config -> + val messageJSON = chain.message().let { + config.jsonAdapter.writeToJson(it, object : RudderTypeAdapter() {}) + } + val messageSize = messageJSON.toString().getUTF8Length() + if (messageSize > MAX_EVENT_SIZE) { + config.logger.error(log = "Event size exceeds the maximum size of $MAX_EVENT_SIZE bytes. Dropping the event.") + return chain.message() + } + } + return chain.proceed(chain.message()) + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/GDPRPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/GDPRPlugin.kt new file mode 100644 index 000000000..1d07c740a --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/GDPRPlugin.kt @@ -0,0 +1,22 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message + +/** + * If opted out, msg won't go forward, will return from here. + * + */ +internal class GDPRPlugin : Plugin { + + override lateinit var analytics: Analytics + + override fun intercept(chain: Plugin.Chain): Message { + return if (analytics.storage.isOptedOut) { + chain.message() + } else { + chain.proceed(chain.message()) + } + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/RudderOptionPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/RudderOptionPlugin.kt new file mode 100644 index 000000000..265674eb2 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/RudderOptionPlugin.kt @@ -0,0 +1,94 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderOption +import com.rudderstack.core.minusWrtKeys +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.MessageContext +import com.rudderstack.core.models.externalIds +import com.rudderstack.core.models.updateWith + +/** + * Alters flow and adds values to [Message] depending on options. + * Manipulates the list of destination plugins based on options + * + * Individual plugin option takes precedence over "All" + * For eg. If "All" is set to true, but a certain integration is set to false, + * that integration will be dumped and not used. + * Likewise if "All" is set to false but a particular integration is set to true, we use it. + * + * In case of [MessageContext] values of [Message.context] and [RudderOption] are merged together. + * + * @param options + */ +internal class RudderOptionPlugin(private val options: RudderOption) : Plugin { + + override lateinit var analytics: Analytics + + override fun intercept(chain: Plugin.Chain): Message { + val msg = chain.message().let { oldMsg -> + oldMsg.copy( + context = oldMsg.updateContext(options), + ) + } + val validIntegrations = validIntegrations() + msg.integrations = validIntegrations + return chain.plugins.takeIf { + it.isNotEmpty() + }?.let { plugins -> + val validPlugins = filterWithAllowedDestinationPlugins( + plugins, + msg.integrations ?: mapOf() + ) + return chain.with(validPlugins).proceed(msg) + } ?: chain.proceed(msg) + + } + + /** + * This filter approves any plugin that's either not a destination plugin, or is + * an allowed destination + * + * @param plugins the plugin list to filter + * @param integrations the map that contains relevant information + * @return the valid [Plugin] list + */ + private fun filterWithAllowedDestinationPlugins( + plugins: List, + integrations: Map + ): List { + return plugins.filter { + it !is DestinationPlugin<*> || (integrations[it.name] ?: integrations["All"] ?:true) + } + + } + + private fun validIntegrations(): Map { + return options.integrations.ifEmpty { mapOf("All" to true) } + } + + private fun Message.updateContext(options: RudderOption): MessageContext? { + //external ids can be present in both message or options. + //save concatenated external ids, if present with both message and options + val messageExternalIds = context?.externalIds + val updatedExternalIds: List>? = + when { + options.externalIds.isEmpty() -> { + messageExternalIds + } + messageExternalIds == null -> { + options.externalIds + } + else -> {//preference is given to external ids in Message + val extraIdsInOptions = + options.externalIds minusWrtKeys messageExternalIds + //adding these to messageExternalIds gives our required ids + messageExternalIds + extraIdsInOptions + } + } + + return context?.updateWith(externalIds = updatedExternalIds) + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/StoragePlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/StoragePlugin.kt new file mode 100644 index 000000000..0f16f351d --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/StoragePlugin.kt @@ -0,0 +1,23 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.models.Message + +/** + * Adds [Message] to repository for further processing. + * Used for cloud mode destinations. + * Saves user related data in case of identify messages and + * + */ +internal class StoragePlugin : Plugin { + + override lateinit var analytics: Analytics + + override fun intercept(chain: Plugin.Chain): Message { + val message = chain.message() + analytics.storage.saveMessage(message.copy()) + return chain.proceed(message) + } + +} diff --git a/core/src/main/java/com/rudderstack/core/internal/plugins/WakeupActionPlugin.kt b/core/src/main/java/com/rudderstack/core/internal/plugins/WakeupActionPlugin.kt new file mode 100644 index 000000000..78c0d790d --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/plugins/WakeupActionPlugin.kt @@ -0,0 +1,37 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.internal.states.DestinationConfigState +import com.rudderstack.core.models.Message + +/** + * Must be added prior to destination plugins. + * Will store messages till all factories are ready + * After that reiterate the messages to the plugins + */ +internal class WakeupActionPlugin : Plugin { + + override lateinit var analytics: Analytics + + private val Analytics.destinationConfigState: DestinationConfigState? + get() = retrieveState() + + override fun intercept(chain: Plugin.Chain): Message { + val destinationConfig = analytics.destinationConfigState?.value + + val forwardChain = + if (destinationConfig == null || !destinationConfig.allIntegrationsReady || analytics.storage.startupQueue.isNotEmpty()) { + analytics.storage.saveStartupMessageInQueue(chain.message()) + //remove all destination plugins that are not ready, for others the message flow is normal + val validPlugins = chain.plugins.toMutableList().filterNot { + it is DestinationPlugin<*> && destinationConfig?.isIntegrationReady(it.name) != true + } + chain.with(validPlugins) + } else chain + return forwardChain.proceed(chain.message()) + + } +} diff --git a/core/src/main/java/com/rudderstack/core/internal/states/ConfigurationsState.kt b/core/src/main/java/com/rudderstack/core/internal/states/ConfigurationsState.kt new file mode 100644 index 000000000..99161f681 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/states/ConfigurationsState.kt @@ -0,0 +1,21 @@ +/* + * Creator: Debanjan Chatterjee on 29/12/21, 5:38 PM Last modified: 29/12/21, 5:37 PM + * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.internal.states + +import com.rudderstack.core.Configuration +import com.rudderstack.core.State + +internal class ConfigurationsState(initialConfiguration: Configuration? = null) : + State(initialConfiguration) {} \ No newline at end of file diff --git a/core/src/main/java/com/rudderstack/core/internal/states/DestinationConfigState.kt b/core/src/main/java/com/rudderstack/core/internal/states/DestinationConfigState.kt new file mode 100644 index 000000000..6763ee9b3 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/internal/states/DestinationConfigState.kt @@ -0,0 +1,23 @@ +/* + * Creator: Debanjan Chatterjee on 13/01/22, 6:01 PM Last modified: 13/01/22, 6:01 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.internal.states + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DestinationConfig +import com.rudderstack.core.State +import com.rudderstack.core.holder.retrieveState + +internal class DestinationConfigState(destinationConfig: DestinationConfig? = null) : + State(destinationConfig?:DestinationConfig()) diff --git a/core/src/main/java/com/rudderstack/core/models/AppVersion.kt b/core/src/main/java/com/rudderstack/core/models/AppVersion.kt new file mode 100644 index 000000000..4482e9854 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/AppVersion.kt @@ -0,0 +1,8 @@ +package com.rudderstack.core.models + +data class AppVersion( + val previousBuild: Int, + val previousVersionName: String, + val currentBuild: Int, + val currentVersionName: String, +) diff --git a/core/src/main/java/com/rudderstack/core/models/Constants.kt b/core/src/main/java/com/rudderstack/core/models/Constants.kt new file mode 100644 index 000000000..77937d923 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/Constants.kt @@ -0,0 +1,7 @@ +package com.rudderstack.core.models + +object Constants { + internal const val TRAITS_ID = "traits" + internal const val EXTERNAL_ID = "externalId" + internal const val CUSTOM_CONTEXT_MAP_ID = "customContextMap" +} diff --git a/core/src/main/java/com/rudderstack/core/models/Extensions.kt b/core/src/main/java/com/rudderstack/core/models/Extensions.kt new file mode 100644 index 000000000..1de6ebf7a --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/Extensions.kt @@ -0,0 +1,92 @@ +@file:JvmName("MessageUtils") + +package com.rudderstack.core.models + +/** + * To be used to extract traits from context of [Message] + */ +val MessageContext.traits: Map? + get() = get(Constants.TRAITS_ID) as? Map? + +/** + * To be used to extract external ids from context of [Message] + */ +val MessageContext.externalIds: List>? + get() = get(Constants.EXTERNAL_ID) as? List>? + +/** + * To be used to extract custom contexts from context of [Message] + */ +val MessageContext.customContexts: Map? + get() = get(Constants.CUSTOM_CONTEXT_MAP_ID) as? Map? + +fun MessageContext.withExternalIdsRemoved() = this - Constants.EXTERNAL_ID + +/** + * Priority is given to this context and not to the operand. + * FOr eg, if the calling context and operand both have same keys in their traits, + * the value assigned to the key is of the calling context one. + * + * @param context + * @return + */ +infix fun MessageContext?.optAddContext(context: MessageContext?): MessageContext? { + //this gets priority + if (this == null) return context + else if (context == null) return this + val newTraits = context.traits?.let { + (it - (this.traits?.keys ?: setOf()).toSet()) optAdd this.traits + } ?: traits + val newCustomContexts = context.customContexts?.let { + (it - (this.customContexts?.keys ?: setOf()).toSet()) optAdd this.customContexts + } ?: customContexts + val newExternalIds = context.externalIds?.let { savedExternalIds -> + val currentExternalIds = this.externalIds ?: emptyList() + val filteredSavedExternalIds = savedExternalIds.filter { savedExternalId -> + currentExternalIds.none { currentExternalId -> + currentExternalId["type"] == savedExternalId["type"] + } + } + currentExternalIds + filteredSavedExternalIds + } ?: externalIds + + createContext(newTraits, newExternalIds, newCustomContexts).let { + //add the extra info from both contexts + val extraOperandContext = context - it.keys + val extraThisContext = this - it.keys + return it + extraOperandContext + extraThisContext + } + +} + +private infix fun Iterable>.minusWrtKeys( + operand: + Iterable> +): List> { + operand.toSet().let { op -> + return this.filterNot { + op inWrtKeys it + } + } +} + +/** + * infix function to match "in" condition for a map inside a list of maps based on just keys. + * ``` + * val list = listOf( + * mapOf("1" to 1), + * mapOf("2" to 3), + * mapOf("4" to 4) + * ) + * list inWrtKeys mapOf("2", "2") //returns true + * ``` + * @param item The item to be checked + * @return true if the map with same keys are present, false otherwise + */ +infix fun Iterable>.inWrtKeys(item: Map): Boolean { + this.toSet().forEach { + if (it.keys.containsAll(item.keys)) + return true + } + return false +} diff --git a/core/src/main/java/com/rudderstack/core/models/Message.kt b/core/src/main/java/com/rudderstack/core/models/Message.kt new file mode 100644 index 000000000..529456098 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/Message.kt @@ -0,0 +1,791 @@ +@file:Suppress("FunctionName") + +package com.rudderstack.core.models + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.rudderstack.core.models.Message.EventType +import com.squareup.moshi.Json +import java.util.Locale +import java.util.UUID + +typealias MessageContext = Map +typealias PageProperties = Map +typealias ScreenProperties = Map +typealias TrackProperties = Map +typealias IdentifyTraits = Map +typealias IdentifyProperties = Map + +// integrations might change from String,Boolean to String, Object at a later point of time +typealias MessageIntegrations = Map + +typealias MessageDestinationProps = Map> +typealias GroupTraits = Map + +@JsonIgnoreProperties("type\$models") +@JsonInclude(JsonInclude.Include.NON_NULL) +sealed class Message( + + /** + * @return Type of event + * @see EventType + */ + @SerializedName("type") + // @Expose + @param:JsonProperty("type") @field:JsonProperty("type") @get:JsonProperty("type") @Json(name = "type") internal var type: EventType, + + // @Expose + // convert to json to put any object as value + @SerializedName("context") @JsonProperty("context") @Json(name = "context") val context: MessageContext? = null, + + // @Expose + @SerializedName("anonymousId") @JsonProperty("anonymousId") @Json(name = "anonymousId") var anonymousId: String?, + + /** @return User ID for the event */ + // @Expose + @SerializedName("userId") @JsonProperty("userId") @Json(name = "userId") var userId: String? = null, + + // format - yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + // @Expose + @SerializedName("originalTimestamp") @field:JsonProperty("originalTimestamp") @get:JsonProperty( + "originalTimestamp" + ) @Json(name = "originalTimestamp") val timestamp: String, + + // @Expose + @SerializedName("destinationProps") @JsonProperty("destinationProps") @Json(name = "destinationProps") val destinationProps: MessageDestinationProps? = null, + +// @Transient + _messageId: String? = null, + _channel: String? = null, +) { + // @Expose + @SerializedName("messageId") + @JsonProperty("messageId") + @Json(name = "messageId") + val messageId: String = _messageId ?: String.format( + Locale.US, + "%d-%s", + System.currentTimeMillis(), + UUID.randomUUID().toString(), + ) + + // ugly hack for moshi + // https://github.com/square/moshi/issues/609#issuecomment-798805367 + @Json(name = "channel") + @JsonProperty("channel") + @SerializedName("channel") + var channel: String = _channel ?: "server" + set(value) { + field = value + } + get() = field ?: "server" // required for gson, cause it might set the property null + // through reflection + + @JsonIgnore + fun getType() = type + + /** + * For internal use. Any value set over here will be overwritten + * internally. For setting custom configuration for integrations for a + * particular message, use RudderOptions + */ + // @Expose + @SerializedName("integrations") + @JsonProperty("integrations") + @Json(name = "integrations") + var integrations: MessageIntegrations? = null + + /** + * For internal usage. This variable will return null when called. + * Setting this variable will make no difference as this value will be overridden while + * syncing the data with server. + */ + @SerializedName("sentAt") + @JsonProperty("sentAt") + @Json(name = "sentAt") + var sentAt: String? = null + open fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + ): Message = when (this) { + is AliasMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + previousId, + ) + + is GroupMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + groupId, + traits, + ) + + is IdentifyMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + properties, + ) + + is PageMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + name, + properties, + category, + ) + + is ScreenMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + properties = properties, + ) + + is TrackMessage -> copy( + context, + anonymousId, + userId, + timestamp, + destinationProps, + eventName, + properties, + ) + }.also { + it.integrations = integrations + it.channel = channel + } + + enum class EventType(val value: String) { + @SerializedName("alias") + @JsonProperty("alias") + @Json(name = "alias") + ALIAS("alias"), + + @SerializedName("group") + @JsonProperty("group") + @Json(name = "group") + GROUP("group"), + + @SerializedName("page") + @JsonProperty("page") + @Json(name = "page") + PAGE("page"), + + @SerializedName("screen") + @JsonProperty("screen") + @Json(name = "screen") + SCREEN("screen"), + + @SerializedName("track") + @JsonProperty("track") + @Json(name = "track") + TRACK("track"), + + @SerializedName("identify") + @JsonProperty("identify") + @Json(name = "identify") + IDENTIFY("identify"), ; + + companion object { + fun fromValue(value: String) = when (value.lowercase()) { + "alias" -> EventType.ALIAS + "group" -> EventType.GROUP + "page" -> EventType.PAGE + "screen" -> EventType.SCREEN + "track" -> EventType.TRACK + "identify" -> EventType.IDENTIFY + else -> throw IllegalArgumentException("Wrong value for event type") + } + } + } + + override fun toString(): String { + return "type = $type, " + "messageId = $messageId, " + "context = $context, " + "anonymousId = $anonymousId, " + "userId = $userId, " + "timestamp = $timestamp, " + "destinationProps = $destinationProps, " + "integrations = $integrations, " + "channel = $channel" + } + + override fun equals(other: Any?): Boolean { + return other is Message && other.type === this.type && other.messageId == this.messageId && other.context == this.context && other.anonymousId == this.anonymousId && other.userId == this.userId && other.timestamp == this.timestamp && other.destinationProps == this.destinationProps && other.integrations == this.integrations && other.channel == this.channel + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + messageId.hashCode() + result = 31 * result + (context?.hashCode() ?: 0) + result = 31 * result + anonymousId.hashCode() + result = 31 * result + (userId?.hashCode() ?: 0) + result = 31 * result + timestamp.hashCode() + result = 31 * result + (destinationProps?.hashCode() ?: 0) + result = 31 * result + integrations.hashCode() + return result + } +} + +class AliasMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") timestamp: String, + + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + @SerializedName("previousId") @JsonProperty("previousId") @Json(name = "previousId") var previousId: String? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, +) : Message( + EventType.ALIAS, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + companion object { + @JvmStatic + fun create( + timestamp: String, + + anonymousId: String? = null, + userId: String? = null, + + destinationProps: MessageDestinationProps? = null, + previousId: String? = null, + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + + ) = AliasMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, + userId, + timestamp, + destinationProps, + previousId, + _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + + destinationProps: MessageDestinationProps? = this.destinationProps, + previousId: String? = this.previousId, + + ) = AliasMessage( + context, + anonymousId, + userId, + timestamp, + destinationProps, + previousId, + _messageId = this.messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "previousId = $previousId" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is AliasMessage && other.previousId == previousId + } + + override fun hashCode(): Int { + return super.hashCode() * (previousId?.hashCode() ?: 1) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +class GroupMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") timestamp: String, + + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + /** @return Group ID for the event */ + @SerializedName("groupId") @JsonProperty("groupId") @Json(name = "groupId") var groupId: String? = null, + + @SerializedName("traits") @JsonProperty("traits") @Json(name = "traits") val traits: GroupTraits? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, + + ) : Message( + EventType.GROUP, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + companion object { + @JvmStatic + fun create( + anonymousId: String? = null, + userId: String? = null, + timestamp: String, + + destinationProps: MessageDestinationProps? = null, + groupId: String?, + groupTraits: GroupTraits?, + + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + ) = GroupMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, + userId, + timestamp, + destinationProps, + groupId, + groupTraits, + _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + + destinationProps: MessageDestinationProps? = this.destinationProps, + groupId: String? = this.groupId, + traits: GroupTraits? = this.traits, + ) = GroupMessage( + context, + anonymousId, + userId, + timestamp, + destinationProps, + groupId, + traits, + _messageId = this.messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "groupId = $groupId, " + "traits = $traits" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is GroupMessage && other.groupId == groupId && other.traits == traits + } + + override fun hashCode(): Int { + var result = groupId?.hashCode() ?: 0 + result = 31 * result + (traits?.hashCode() ?: 0) + return result + } +} + +class PageMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") timestamp: String, + + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + /** @return Name of the event tracked */ + + @SerializedName("event") @get:JsonProperty("event") @field:JsonProperty("event") @param:JsonProperty( + "event" + ) @Json(name = "event") var name: String? = null, + + /** + * Get the properties back as set to the event Always convert objects to + * it's json equivalent before setting it as values + * + * @return Map of String-Object + */ + + @SerializedName("properties") @JsonProperty("properties") @Json(name = "properties") val properties: PageProperties? = null, + + @SerializedName("category") @JsonProperty("category") @Json(name = "category") val category: String? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, +) : Message( + EventType.PAGE, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + companion object { + @JvmStatic + fun create( + anonymousId: String? = null, + userId: String? = null, + timestamp: String, + destinationProps: MessageDestinationProps? = null, + name: String? = null, + properties: PageProperties? = null, + category: String? = null, + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + ) = PageMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, userId, timestamp, destinationProps, name, properties, + category, _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + destinationProps: MessageDestinationProps? = this.destinationProps, + name: String? = this.name, + properties: PageProperties? = this.properties, + category: String? = this.category, + + ) = PageMessage( + context, anonymousId, userId, timestamp, destinationProps, name, properties, + category, _messageId = this.messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "name = $name, " + "properties = $properties, " + "category = $category" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is PageMessage && other.name == name && other.properties == properties && other.category == category + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (properties?.hashCode() ?: 0) + result = 31 * result + (category?.hashCode() ?: 0) + return result + } +} + +class ScreenMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") timestamp: String, + + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + + /** + * Get the properties back as set to the event Always convert objects to + * it's json equivalent before setting it as values + * + * @return Map of String-Object + */ + + @SerializedName("properties") @JsonProperty("properties") @Json(name = "properties") val properties: ScreenProperties? = null, + @SerializedName("event") @field:JsonProperty("event") @param:JsonProperty("event") @get:JsonProperty( + "event" + ) @Json(name = "event") val eventName: String? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, +) : Message( + EventType.SCREEN, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + companion object { + @JvmStatic + fun create( + name: String, + timestamp: String, + anonymousId: String? = null, + userId: String? = null, + destinationProps: MessageDestinationProps? = null, + category: String? = null, + properties: ScreenProperties? = null, + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + ) = ScreenMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, userId, timestamp, destinationProps, + (properties ?: ScreenProperties()).let { + if (name != null) it.plus("name" to name) else it + }.let { + if (category != null) it.plus("category" to category) else it + }, + name, + _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + destinationProps: MessageDestinationProps? = this.destinationProps, + name: String? = this.properties?.get("name") as String?, + category: String? = this.properties?.get("category") as String?, + properties: ScreenProperties? = this.properties, + eventName: String? = this.eventName, + ) = ScreenMessage( + context, anonymousId, userId, timestamp, destinationProps, + (properties ?: ScreenProperties()).let { + if (name != null) it.plus("name" to name) else it + }.let { + if (category != null) it.plus("category" to category) else it + }, + eventName, + _messageId = this.messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "properties = $properties" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is ScreenMessage && other.properties == properties + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (properties?.hashCode() ?: 0) + return result + } +} + +@JsonIgnoreProperties(ignoreUnknown = true, allowSetters = true) +class TrackMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") + timestamp: String, + /*channel: String? = null,*/ + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + /** @return Name of the event tracked */ + + @SerializedName("event") @field:JsonProperty("event") @param:JsonProperty("event") @get:JsonProperty( + "event" + ) @Json(name = "event") val eventName: String? = null, + /** + * Get the properties back as set to the event Always convert objects to + * it's json equivalent before setting it as values + * + * @return Map of String-Object + */ + + @SerializedName("properties") @field:JsonProperty("properties") @param:JsonProperty("properties") @get:JsonProperty( + "properties" + ) @Json(name = "properties") val properties: TrackProperties? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, +) : Message( + EventType.TRACK, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + companion object { + @JvmStatic + fun create( + eventName: String?, + timestamp: String, + properties: TrackProperties? = null, + anonymousId: String? = null, + userId: String? = null, + destinationProps: MessageDestinationProps? = null, + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + ) = TrackMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, + userId, + timestamp, + destinationProps, + eventName, + properties, + _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + destinationProps: MessageDestinationProps? = this.destinationProps, + eventName: String? = this.eventName, + properties: TrackProperties? = this.properties, + ) = TrackMessage( + context, + anonymousId, + userId, + timestamp, + destinationProps, + eventName, + properties, + _messageId = this.messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "eventName = $eventName, " + "properties = $properties" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is TrackMessage && other.eventName == eventName && other.properties == properties + } + + override fun hashCode(): Int { + var result = (eventName?.hashCode() ?: 0) + result = 31 * result + (properties?.hashCode() ?: 0) + return result + } +} + +class IdentifyMessage internal constructor( + + @JsonProperty("context") @Json(name = "context") context: MessageContext? = null, + @JsonProperty("anonymousId") @Json(name = "anonymousId") anonymousId: String?, + @JsonProperty("userId") @Json(name = "userId") userId: String? = null, + @JsonProperty("originalTimestamp") @Json(name = "originalTimestamp") timestamp: String, + + @JsonProperty("destinationProps") @Json(name = "destinationProps") destinationProps: MessageDestinationProps? = null, + /** + * Get the properties back as set to the event Always convert objects to + * it's json equivalent before setting it as values + */ + + @SerializedName("properties") @JsonProperty("properties") @Json(name = "properties") val properties: IdentifyProperties? = null, + @JsonProperty("not_applicable", required = false) // work-around to ignore value param + // jackson serialisation + _messageId: String? = null, +) : Message( + EventType.IDENTIFY, + context, + anonymousId, + userId, + timestamp, + destinationProps, + _messageId, +) { + + companion object { + @JvmStatic + fun create( + anonymousId: String? = null, + userId: String? = null, + timestamp: String, + properties: IdentifyProperties? = null, + destinationProps: MessageDestinationProps? = null, + traits: IdentifyTraits? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + _messageId: String? = null, + ) = IdentifyMessage( + createContext(traits, externalIds, customContextMap), + anonymousId, + userId, + timestamp, + destinationProps, + properties, + _messageId, + ) + } + + fun copy( + context: MessageContext? = this.context, + anonymousId: String? = this.anonymousId, + userId: String? = this.userId, + timestamp: String = this.timestamp, + destinationProps: MessageDestinationProps? = this.destinationProps, + properties: IdentifyProperties? = this.properties, + + ) = IdentifyMessage( + context, + anonymousId, + userId, + timestamp, + destinationProps, + properties, + _messageId = messageId, + ) + + override fun toString(): String { + return "${super.toString()}, " + "properties = $properties" + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) && other is IdentifyMessage && other.properties == properties + } + + override fun hashCode(): Int { + return properties?.hashCode() ?: 0 + } +} +// verbose methods +// verbose methods + +fun TrackProperties(vararg keyPropertyPair: Pair): TrackProperties = + mapOf(*keyPropertyPair) + +// fun PageProperties(vararg keyPropertyPair: Pair) : PageProperties = mapOf(*keyPropertyPair) + +fun ScreenProperties(vararg keyPropertyPair: Pair): ScreenProperties = + mapOf(*keyPropertyPair) + +fun IdentifyProperties(vararg keyPropertyPair: Pair): IdentifyProperties = + mapOf(*keyPropertyPair) + +fun MessageIntegrations(vararg keyPropertyPair: Pair): MessageIntegrations = + mapOf(*keyPropertyPair) + +fun MessageDestinationProps(vararg keyPropertyPair: Pair>): MessageDestinationProps = + mapOf(*keyPropertyPair) + +fun IdentifyTraits(vararg keyPropertyPair: Pair): IdentifyTraits = + mapOf(*keyPropertyPair) + +fun GroupTraits(vararg keyPropertyPair: Pair): GroupTraits = mapOf(*keyPropertyPair) diff --git a/core/src/main/java/com/rudderstack/core/models/RudderServerConfig.kt b/core/src/main/java/com/rudderstack/core/models/RudderServerConfig.kt new file mode 100644 index 000000000..3a67a7364 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/RudderServerConfig.kt @@ -0,0 +1,252 @@ +package com.rudderstack.core.models + +import com.fasterxml.jackson.annotation.JsonProperty +import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.* + +/** + * Configuration of the server + * @property source + */ +data class RudderServerConfig( + @Json(name = "isHosted") + @JsonProperty("isHosted") + @SerializedName("isHosted") + val isHosted: Boolean = false, + + @Json(name = "source") + @JsonProperty("source") + @SerializedName("source") + val source: RudderServerConfigSource? = null, +) : Serializable { + private var _readIsHosted: Boolean? = null + private var _readSource: RudderServerConfigSource? = null + + companion object { + @JvmStatic + private val serialVersionUID: Long = 1 + } + + @Throws(ClassNotFoundException::class, IOException::class) + private fun readObject(inputStream: ObjectInputStream) { + _readIsHosted = inputStream.readBoolean() + _readSource = inputStream.readObject() as? RudderServerConfigSource + } + + private fun readResolve(): Any { + return RudderServerConfig(_readIsHosted ?: false, _readSource) + } + + @Throws(IOException::class) + private fun writeObject(outputStream: ObjectOutputStream) { + outputStream.writeBoolean(isHosted) + outputStream.writeObject(source) + } + + + /** + * Configuration of source + * + * @property sourceId + * @property sourceName + * @property isSourceEnabled + * @property updatedAt + * @property destinations + */ + data class RudderServerConfigSource( + @Json(name = "id") + @JsonProperty("id") + @SerializedName("id") + val sourceId: String? = null, + + @Json(name = "name") + @JsonProperty("name") + @SerializedName("name") + val sourceName: String? = null, + + @Json(name = "enabled") + @JsonProperty("enabled") + @SerializedName("enabled") + val isSourceEnabled: Boolean = false, + + @Json(name = "updatedAt") + @JsonProperty("updatedAt") + @SerializedName("updatedAt") + val updatedAt: String? = null, + + @Json(name = "destinations") + @JsonProperty("destinations") + @SerializedName("destinations") + val destinations: List? = null, + ) : Serializable { + companion object { + @JvmStatic + private val serialVersionUID: Long = 2 + } + + private var _readSourceId: String? = null + private var _readSourceName: String? = null + private var _readIsSourceEnabled: Boolean? = null + private var _readUpdatedAt: String? = null + private var _readDestinations: List? = null + + @Throws(ClassNotFoundException::class, IOException::class) + private fun readObject(inputStream: ObjectInputStream) { + _readSourceId = inputStream.readObject() as? String + _readSourceName = inputStream.readObject() as? String + _readIsSourceEnabled = inputStream.readBoolean() + _readUpdatedAt = inputStream.readObject() as? String + _readDestinations = inputStream.readObject() as? List + } + + private fun readResolve(): Any { + return RudderServerConfigSource( + _readSourceId, _readSourceName, _readIsSourceEnabled ?: false, _readUpdatedAt, _readDestinations + ) + } + + @Throws(IOException::class) + private fun writeObject(outputStream: ObjectOutputStream) { + outputStream.writeObject(sourceId) + outputStream.writeObject(sourceName) + outputStream.writeBoolean(isSourceEnabled) + outputStream.writeObject(updatedAt) + outputStream.writeObject(destinations) + } + } + + data class RudderServerDestination( + @Json(name = "id") + @JsonProperty("id") + @SerializedName("id") + val destinationId: String, + + @Json(name = "name") + @JsonProperty("name") + @SerializedName("name") + val destinationName: String? = null, + + @Json(name = "enabled") + @JsonProperty("enabled") + @SerializedName("enabled") + val isDestinationEnabled: Boolean = false, + + @Json(name = "updatedAt") + @JsonProperty("updatedAt") + @SerializedName("updatedAt") + val updatedAt: String? = null, + + @Json(name = "destinationDefinition") + @JsonProperty("destinationDefinition") + @SerializedName("destinationDefinition") + val destinationDefinition: RudderServerDestinationDefinition? = null, + + @Json(name = "config") + @JsonProperty("config") + @SerializedName("config") + val destinationConfig: Map, + + @JsonProperty("areTransformationsConnected") + @Json(name = "areTransformationsConnected") + val areTransformationsConnected: Boolean = false, + ) : Serializable { + companion object { + @JvmStatic + private val serialVersionUID: Long = 3 + } + + private var _readDestinationId: String? = null + private var _readDestinationName: String? = null + private var _readIsDestinationEnabled: Boolean = false + private var _readUpdatedAt: String? = null + private var _readDestinationDefinition: RudderServerDestinationDefinition? = null + private var _readDestinationConfig: Map = mapOf() + private var _readAreTransformationsConnected: Boolean = false + + @Throws(ClassNotFoundException::class, IOException::class) + private fun readObject(inputStream: ObjectInputStream) { + _readDestinationId = inputStream.readObject() as? String + _readDestinationName = inputStream.readObject() as? String + _readIsDestinationEnabled = inputStream.readBoolean() + _readUpdatedAt = inputStream.readObject() as? String + _readDestinationDefinition = inputStream.readObject() as? RudderServerDestinationDefinition + _readDestinationConfig = (inputStream.readObject() as? Map) ?: mapOf() + _readAreTransformationsConnected = inputStream.readBoolean() + } + + private fun readResolve(): Any { + return RudderServerDestination( + _readDestinationId ?: "", + _readDestinationName, + _readIsDestinationEnabled, + _readUpdatedAt, + _readDestinationDefinition, + _readDestinationConfig, + _readAreTransformationsConnected + ) + } + + @Throws(IOException::class) + private fun writeObject(outputStream: ObjectOutputStream) { + outputStream.writeObject(destinationId) + outputStream.writeObject(destinationName) + outputStream.writeBoolean(isDestinationEnabled) + outputStream.writeObject(updatedAt) + outputStream.writeObject(destinationDefinition) + outputStream.writeObject(destinationConfig) + outputStream.writeBoolean(areTransformationsConnected) + } + } + + data class RudderServerDestinationDefinition( + @Json(name = "name") + @JsonProperty("name") + @SerializedName("name") + val definitionName: String? = null, + + @Json(name = "displayName") + @JsonProperty("displayName") + @SerializedName("displayName") + val displayName: String? = null, + + @Json(name = "updatedAt") + @JsonProperty("updatedAt") + @SerializedName("updatedAt") + val updatedAt: String? = null, + ) : Serializable { + companion object { + @JvmStatic + private val serialVersionUID: Long = 4 + } + + private var _readDefinitionName: String? = null + private var _readDisplayName: String? = null + private var _readUpdatedAt: String? = null + + @Throws(ClassNotFoundException::class, IOException::class) + private fun readObject(inputStream: ObjectInputStream) { + _readDefinitionName = inputStream.readObject() as? String + _readDisplayName = inputStream.readObject() as? String + _readUpdatedAt = inputStream.readObject() as? String + } + + private fun readResolve(): Any { + return RudderServerDestinationDefinition( + _readDefinitionName, _readDisplayName, _readUpdatedAt + ) + } + + @Throws(IOException::class) + private fun writeObject(outputStream: ObjectOutputStream) { + outputStream.writeObject(definitionName) + outputStream.writeObject(displayName) + outputStream.writeObject(updatedAt) + } + } + +} diff --git a/core/src/main/java/com/rudderstack/core/models/Utils.kt b/core/src/main/java/com/rudderstack/core/models/Utils.kt new file mode 100644 index 000000000..32eae2132 --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/models/Utils.kt @@ -0,0 +1,60 @@ +@file:JvmName("Utils") + +package com.rudderstack.core.models + +/** + * An utility function to create a context Map + * + * @param traits Properties for groups and users + * @param externalIds Custom ids used for specific destinations + * @param customContextMap + * @param contextAddOns any extra items added as it is. + * @return + */ +fun createContext( + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + contextAddOns: Map? = null, +): MessageContext = + mapOf( + Constants.TRAITS_ID to traits, + Constants.EXTERNAL_ID to externalIds, + Constants.CUSTOM_CONTEXT_MAP_ID to customContextMap, + ).let { + if (contextAddOns == null) it else it + contextAddOns + } + +/** + * Updates the values of this context with non null values from new context + * + * @param newContext + * @return updated context + */ +fun MessageContext.updateWith(newContext: MessageContext): MessageContext { + return (this optAdd newContext.filterValues { it != null }) ?: mapOf() +} + +fun MessageContext.updateWith( + traits: Map? = null, + externalIds: List>? = null, + customContextMap: Map? = null, + contextAddOns: Map? = null, +): MessageContext { + return (this optAdd + traits?.let { (Constants.TRAITS_ID to it) } optAdd + externalIds?.let { (Constants.EXTERNAL_ID to it) } optAdd + customContextMap?.let { (Constants.CUSTOM_CONTEXT_MAP_ID to it) } optAdd + contextAddOns)?: mapOf() +} + +internal infix fun Map.optAdd(pair: Pair?): Map { + return pair?.let { + this + it + } ?: this +} +internal infix fun Map.optAdd(map: Map?): Map { + return map?.let { + this + it + } ?: this +} diff --git a/core/src/main/java/com/rudderstack/core/utilities/annotations.kt b/core/src/main/java/com/rudderstack/core/utilities/annotations.kt new file mode 100644 index 000000000..f0fd84c7d --- /dev/null +++ b/core/src/main/java/com/rudderstack/core/utilities/annotations.kt @@ -0,0 +1,5 @@ +package com.rudderstack.core.utilities + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +annotation class FutureUse(val info: String) diff --git a/core/src/test/java/com/rudderstack/core/AnalyticsTest.kt b/core/src/test/java/com/rudderstack/core/AnalyticsTest.kt new file mode 100644 index 000000000..6a592d084 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/AnalyticsTest.kt @@ -0,0 +1,971 @@ +package com.rudderstack.core + +import com.rudderstack.core.RudderUtils.getUTF8Length +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.internal.states.DestinationConfigState +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.externalIds +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import com.rudderstack.web.HttpResponse +import com.vagabond.testcommon.assertArgument +import com.vagabond.testcommon.generateTestAnalytics +import junit.framework.TestSuite +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.aMapWithSize +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.everyItem +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.`in` +import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.iterableWithSize +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyList +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread +import kotlin.math.ceil + +abstract class AnalyticsTest { + //test track, alias, identify, group + //wake up working, + //rudder options work + //test anon_id + //complete flow + protected abstract val jsonAdapter: JsonAdapter + private lateinit var analytics: Analytics + private lateinit var storage: Storage + private val mockServerConfig = RudderServerConfig( + source = RudderServerConfig.RudderServerConfigSource( + isSourceEnabled = true, + destinations = listOf( + RudderServerConfig.RudderServerDestination( + destinationName = "assert-destination", + isDestinationEnabled = true, + destinationId = "1234", + destinationConfig = mapOf("eventFilteringOption" to "disable"), + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + "enabled-destination", displayName = "enabled-destination" + ) + ) + ), + ) + ) + private lateinit var mockedControlPlane: ConfigDownloadService + private lateinit var mockedDataUploadService: DataUploadService + + @Before + fun setup() { + mockedControlPlane = mock(ConfigDownloadService::class.java).also { + `when`(it.download(any())).then { + + it.getArgument<(success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit>( + 0 + ) + .invoke( + true, mockServerConfig, null + ) + } + } + mockedDataUploadService = mock(com.rudderstack.core.DataUploadService::class.java) + storage = BasicStorageImpl() + val mockedResponse: HttpResponse = HttpResponse(200, "OK", null) + mockedDataUploadService.let { + + whenever(it.upload(any(), any(), any())).then { + it.getArgument<(response: HttpResponse) -> Unit>(2).invoke( + mockedResponse + ) + } + `when`(it.uploadSync(any>(), anyOrNull())).thenReturn( + mockedResponse + ) + } + analytics = Analytics( + writeKey, + Configuration( + jsonAdapter, + shouldVerifySdk = false + ), + storage = storage, + initializationListener = { success, message -> + assertThat(success, `is`(true)) + }, + dataUploadService = mockedDataUploadService, + configDownloadService = mockedControlPlane + ) + println("setup done with analytics: $analytics, shouldVerifySdk = ${analytics.currentConfiguration?.shouldVerifySdk}") + } + + @After + fun destroy() { + analytics.clearStorage() + analytics.shutdown() + println("After called on analytics $analytics") + } + + @Test + fun `test Analytics initialization listener call with correct write key`() { + println("running test test Analytics initialization listener call with correct write key") + analytics.shutdown() + val isDone = AtomicBoolean(false) + analytics = Analytics( + writeKey, Configuration( + jsonAdapter, shouldVerifySdk = true + ), initializationListener = { success, message -> + assertThat(success, `is`(true)) + assertThat(message, nullValue()) + isDone.set(true) + }, configDownloadService = mockedControlPlane + ) + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilTrue(isDone) + } + + @Test + fun `test Analytics initialization listener call with incorrect write key`() { + println("running test test Analytics initialization listener call with incorrect write key") + val isDone = AtomicBoolean(false) + analytics.shutdown() + val mockedControlPlane = mock(ConfigDownloadService::class.java).also { + `when`(it.download(any())).then { + + it.getArgument<(success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit>( + 0 + ).invoke( + false, null, "some error" + ) + } + } + analytics = Analytics( + "some wrong write key", Configuration( + jsonAdapter, + sdkVerifyRetryStrategy = RetryStrategy.exponential(1), + shouldVerifySdk = true + ), initializationListener = { success, message -> + assertThat(success, `is`(false)) + assertThat(message, `is`("Downloading failed, Shutting down some error")) + }, configDownloadService = mockedControlPlane, + shutdownHook = { isDone.set(true) } + ) + + + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(isDone) + } + + @Test + fun `test config service not called if shouldVerifySdk is false`() { + println("running test test config service not called if shouldVerifySdk is false") + // default shouldVerifySdk is false + // we wait for some time + // on setup, analytics is initialized + busyWait(200) + verify(mockedControlPlane, never()).download( + any() + ) + } + + @Test + fun `test configuration has proper retry strategy`() { + println("running test test configuration has proper retry strategy") + analytics.shutdown() + val retryStrategy = mock() + analytics = Analytics( + writeKey, Configuration( + jsonAdapter, sdkVerifyRetryStrategy = retryStrategy, shouldVerifySdk = true + ), configDownloadService = mockedControlPlane + ) + assertThat(analytics.currentConfiguration?.sdkVerifyRetryStrategy, `is`(retryStrategy)) + } + + @Test + fun `test identify`() { + println("running test test identify") + analytics.shutdown() + analytics = generateTestAnalytics(Configuration(jsonAdapter)) + analytics.identify("user_id", mapOf("trait-1" to "t-1", "trait-2" to "t-2")) + analytics.assertArgument { input, output -> + println("Input: $input\nOutput: $output") + assertThat( + output, allOf( + notNullValue(), + instanceOf(IdentifyMessage::class.java), + hasProperty("userId", `is`("user_id")), + hasProperty( + "context", allOf( + hasEntry( + equalTo("traits"), allOf( + notNullValue(), + aMapWithSize(3), + hasEntry("trait-1", "t-1"), + hasEntry("trait-2", "t-2"), + ) + ) + ) + ) + ) + ) + } + } + + @Test + fun `test track event`() { + println("running test test track event") + analytics.shutdown() + analytics = generateTestAnalytics(Configuration(jsonAdapter)) + analytics.track { + event("event-1") + userId("user_id") + trackProperties { + add("prop-1" to "p-1") + add("prop-2" to "p-2") + add("trait-1" to "t-1") + } + } + analytics.assertArgument { input, output -> + println("Input: $input\nOutput: $output") + assertThat( + output, allOf( + notNullValue(), + instanceOf(TrackMessage::class.java), + hasProperty("userId", `is`("user_id")), + hasProperty( + "properties", allOf( + aMapWithSize(3), + hasEntry("trait-1", "t-1"), + hasEntry("prop-1", "p-1"), + hasEntry("prop-2", "p-2"), + ) + ) + ) + ) + } + } + + @Test + fun `test alias event`() { + println("running test test alias event") + analytics.shutdown() + analytics = generateTestAnalytics(Configuration(jsonAdapter)) + analytics.alias { + userId("user_id") + newId("new_id") + } + analytics.assertArgument { input, output -> + println("Input: $input\nOutput: $output") + assertThat( + output, allOf( + notNullValue(), + instanceOf(AliasMessage::class.java), + hasProperty("userId", `is`("new_id")), + hasProperty("previousId", `is`("user_id")), + ) + ) + } + } + + fun `test group event`() { + + } + + + @Test + fun `test with later initialized destinations`() { + println("running test test with later initialized destinations") + analytics.shutdown() + analytics = generateTestAnalytics( + Configuration(jsonAdapter, shouldVerifySdk = true), mockedControlPlane + ) + val laterInitDestPlugin = mock(DestinationPlugin::class.java) + whenever(laterInitDestPlugin.name).thenReturn("enabled-destination") + whenever(laterInitDestPlugin.isReady).thenReturn(true) + analytics.track { event("lost-track") } + busyWait(100) + analytics.addPlugin(laterInitDestPlugin) + val chainCaptor = argumentCaptor() + analytics.track( + eventName = "track", + userId = "user_id", + trackProperties = mapOf("prop-1" to "p-1", "prop-2" to "p-2") + ) + busyWait(100)//since it is submitted to executor + + verify(laterInitDestPlugin).intercept(chainCaptor.capture()) + val message = chainCaptor.firstValue.message() + assertThat(message.userId, `is`("user_id")) + assertThat( + message, hasProperty( + "properties", allOf( + aMapWithSize(2), + hasEntry("prop-1", "p-1"), + hasEntry("prop-2", "p-2"), + ) + ) + ) + assertThat( + message, allOf( + instanceOf(TrackMessage::class.java), hasProperty("eventName", `is`("track")) + ) + ) + + } + + @Test + fun `test with rudder option`() { + //given + analytics.shutdown() + analytics = Analytics( + writeKey, + Configuration( + jsonAdapter, shouldVerifySdk = true + ), + storage = storage, + initializationListener = { success, message -> + assertThat(success, `is`(true)) + }, + dataUploadService = mockedDataUploadService, + configDownloadService = mockedControlPlane + ) + while (analytics.retrieveState()?.value == null) { + } + busyWait(300L) // enough for server config to be downloaded + val rudderOption = RudderOption().putExternalId( + "some_type", "some_id" + ).putIntegration("enabled-destination", true) + .putIntegration("All", false) + + val dummyPlugin = mock(BaseDestinationPlugin::class.java) + whenever(dummyPlugin.name).thenReturn("dummy") + whenever(dummyPlugin.isReady).thenReturn(true) + val assertdestination = mock(DestinationPlugin::class.java) + whenever(assertdestination.name).thenReturn("enabled-destination") + whenever(assertdestination.isReady).thenReturn(true) + analytics.addPlugin(assertdestination, dummyPlugin) + + //when + val trackMessage = + TrackMessage.create(eventName = "some", timestamp = RudderUtils.timeStamp) + analytics.track( + trackMessage, options = + rudderOption + ) + val waitUntil = AtomicBoolean(false) + analytics.addCallback(object : Callback { + override fun success(message: Message?) { + waitUntil.set(message is TrackMessage && message.eventName == "some") + } + + override fun failure(message: Message?, throwable: Throwable?) { + waitUntil.set(message is TrackMessage && message.eventName == "some") + } + }) + while (waitUntil.get().not()) { + } + busyWait(500L) + + //then + verify(dummyPlugin, never()).intercept(any()) + val chainArgCaptor = argumentCaptor() + verify(assertdestination, times(1)).intercept(chainArgCaptor.capture()) + val msg = chainArgCaptor.firstValue.message() + assertThat( + msg.integrations, allOf( + aMapWithSize(2), + hasEntry("enabled-destination", true), + hasEntry("All", false), + ) + ) + assertThat( + msg.context?.externalIds, allOf( + notNullValue(), iterableWithSize(1), containsInAnyOrder( + allOf( + aMapWithSize(2), + hasEntry("id", "some_id"), + hasEntry("type", "some_type") + ) + + ) + ) + ) + } + + @Suppress("UNCHECKED_CAST") + @Test + fun `test messages flushed when sent from different threads based on flushQ size`() { + println("running test test messages flushed when sent from different threads based on flushQ size") + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 300, maxFlushInterval = 10_000 + ) + } + + (1..2).map { + thread { + analytics.track("${Thread.currentThread().name}:$it-1") + analytics.track("${Thread.currentThread().name}:$it-2") + } + } + val storageCount = AtomicInteger(0) + while (storageCount.get() < 4) { + storageCount.set(storage.getDataSync().count()) + } + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 3 + ) + } + while (storage.getDataSync().isNotEmpty()) { + } + busyWait(300) + val listCaptor = argumentCaptor>() + verify(mockedDataUploadService, atLeast(1)).uploadSync(listCaptor.capture(), anyOrNull()) + val allMsgsUploaded = listCaptor.allValues.flatten() + assertThat(allMsgsUploaded, allOf(notNullValue(), iterableWithSize(4))) + } + + @Test + fun `test messages flushed when sent from different threads based on periodic timeout`() { + println("running test test messages flushed when sent from different threads based on periodic timeout") + + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 100, + maxFlushInterval = 10000 // so no flush takes place while + ) + } + assertThat(analytics.currentConfiguration?.maxFlushInterval, `is`(10000)) +// val trackedMsgCount = AtomicInteger(0) + + (1..2).map { + thread { + analytics.track("${Thread.currentThread().name}:$it-1") + analytics.track("${Thread.currentThread().name}:$it-2") +// trackedMsgCount.addAndGet(2) + } + } + val dataStoredCount = AtomicInteger(0) + while (dataStoredCount.get() < 4) { + dataStoredCount.set(storage.getDataSync().count()) + } + + dataStoredCount.set(0) + + while (dataStoredCount.get() > 0) { + dataStoredCount.set(storage.getDataSync().count()) + } + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 100, + maxFlushInterval = 10000 // so no flush takes place while + ) + } + assertThat(analytics.currentConfiguration?.maxFlushInterval, `is`(10000)) + val timeNow = System.currentTimeMillis() + while (storage.getDataSync().isNotEmpty()) { + if (System.currentTimeMillis() - timeNow > 10000) break + } + busyWait(200) // enough time for one flush + val listCaptor = argumentCaptor>() + verify(mockedDataUploadService, times(1)).uploadSync(listCaptor.capture(), anyOrNull()) + val allMsgsUploaded = listCaptor.allValues.flatten() + assertThat(allMsgsUploaded, allOf(notNullValue(), iterableWithSize(4))) + } + + @Test + fun `test multiple messages ordering`() { + println("running test test multiple messages ordering") + // add a plugin to check, add some delay to it + val events = (1..10).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + val isDone = AtomicBoolean(false) + var msgCounter = 1 + val assertPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat( + chain.message(), allOf( + Matchers.isA(TrackMessage::class.java), + hasProperty("eventName", `is`("event:${msgCounter++}")) + ) + ) + Thread.sleep(20L) //a minor delay + if (events.size < msgCounter) isDone.set(true) + return chain.proceed(chain.message()) + } + } + analytics.addPlugin(assertPlugin) + for (i in events) { + analytics.track(i) + } + Awaitility.await().atMost(20, TimeUnit.SECONDS).untilTrue(isDone) + } + + @Test + fun `test no item is tracked after shutdown`() { + println("running test test no item is tracked after shutdown") + //flush should be called prior to shutdown. and data uploaded + //we will track few events, less than flush_queue_size, + // call shutdown and wait for sometime to check the storage count. + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 20, + ) + } + val events = (1..10).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + analytics.shutdown() + for (i in events) { + analytics.track(i) + } + verify(mockedDataUploadService, never()).uploadSync(anyList(), anyOrNull()) +// assertThat(storage.getDataSync(), anyOf(nullValue(), iterableWithSize(0))) + } + + @Test + fun `test blocking flush`() { + println("running test test blocking flush") + //we will track few events, less than flush_queue_size, + // call flush and wait for sometime to check the storage count. + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 200, + maxFlushInterval = 10_000_00 + ) + } + val events = (1..10).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + for (i in events) { + analytics.track(i) + } + while ((analytics.storage.getDataSync().size) < 10) { + } + analytics.blockingFlush() + assertThat(storage.getDataSync(), anyOf(nullValue(), iterableWithSize(0))) + verify(mockedDataUploadService, atLeast(1)).uploadSync(anyList(), anyOrNull()) + } + + @Test + fun `given collective batch size is more than MAX_BATCH_SIZE, when blockingFlush is called, then messages are flushed in multiple batches`() { + println("running test given collective batch size is more than MAX_BATCH_SIZE, when blockingFlush is called, then messages are flushed in multiple batches") + + val totalMessages = 100 + val properties = mutableMapOf() + properties["property"] = generateDataOfSize(1024 * 30) + + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 500, maxFlushInterval = 10_000_00 + ) + } + val events = (1..totalMessages).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp, properties = properties) + } + val numberOfBatches = calculateNumberOfBatches(events.first(), totalMessages) + + for (i in events) { + analytics.track(i) + } + while ((analytics.storage.getDataSync().size) < totalMessages) { + } + + analytics.blockingFlush() + + assertThat(storage.getDataSync(), anyOf(nullValue(), iterableWithSize(0))) + verify(mockedDataUploadService, times(numberOfBatches)).uploadSync(anyList(), anyOrNull()) + } + + + @Test + fun `test back pressure strategies`() { + println("running test test back pressure strategies") + //we check the storage directly + val events = (1..20).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + storage.setStorageCapacity(10) + storage.setBackpressureStrategy(Storage.BackPressureStrategy.Drop) // first 10 will be there + storage.saveMessage(*events.toTypedArray()) + val first10Events = events.take(10).map { it.eventName } + val last10Events = events.takeLast(10).map { it.eventName } + + assertThat( + storage.getDataSync(), allOf( + iterableWithSize(10), everyItem( + allOf( + Matchers.isA(TrackMessage::class.java), hasProperty( + "eventName", allOf( + `in`(first10Events), not(`in`(last10Events)) + ) + ) + ) + ) +// contains(last10Events) + ) + ) + storage.clearStorage() + storage.setBackpressureStrategy(Storage.BackPressureStrategy.Latest) // last 10 will be there + storage.saveMessage(*events.toTypedArray()) + assertThat( + storage.getDataSync(), allOf( + iterableWithSize(10), everyItem( + allOf( + Matchers.isA(TrackMessage::class.java), hasProperty( + "eventName", allOf( + `in`(last10Events), not(`in`(first10Events)) + ) + ) + ) + ) +// contains(last10Events) + ) + ) + } + + @Test + fun `test should verify sdk`() { + println("running test test should verify sdk") + val spyControlPlane = spy(ConfigDownloadService::class.java) + Analytics( + writeKey, Configuration( + jsonAdapter + ), configDownloadService = spyControlPlane + ).shutdown() + verify(spyControlPlane, times(0)).download( + any() + ) + } + + @Test + fun `test flush after shutdown`() { + println("running test test flush after shutdown") + analytics.applyConfiguration { + Configuration( + jsonAdapter, + shouldVerifySdk = false, + flushQueueSize = 100, + maxFlushInterval = 10000 + ) + } + val events = (1..5).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + val events2 = (6..10).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + + events.forEach { + analytics.track(it) + } + Thread.sleep(500) + analytics.shutdown() + Thread.sleep(200) // so that data doesn't get inserted while flushing + // This won't happen in production code, as we are specifically doing this through storage. + //In real-world scenario, we would use analytics.saveMessage, which won't work if shutdown is called + //inserting some data + events2.forEach { + storage.saveMessage(it) + } +// Thread.sleep(500) + analytics.flush() + Thread.sleep(200) + verify(mockedDataUploadService, times(0)).uploadSync(any(), anyOrNull()) + } + + @Test + fun `test no force flush after shutdown`() { + println("running test test no force flush after shutdown") + analytics.shutdown() + val events = (1..5).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + val spyDataUploadService = spy(DataUploadService::class.java) + + analytics = Analytics( + writeKey, Configuration( + jsonAdapter + ), storage = storage, initializationListener = { success, message -> + assertThat(success, `is`(true)) + }, dataUploadService = spyDataUploadService, configDownloadService = mockedControlPlane + ) + analytics.shutdown() + busyWait(500) + assertThat(analytics.isShutdown, `is`(true)) + //inserting some data to storage + + storage.saveMessage(*events.toTypedArray()) + + analytics.blockingFlush() +// busyWait(/250) + verify(spyDataUploadService, times(0)).uploadSync(any(), anyOrNull()) + } + + @Test + fun `test shutdown`() { + println("running test test shutdown") + val events = (1..5).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + val someAnalytics = Analytics( + writeKey, + Configuration( + jsonAdapter + ), + storage = storage, + initializationListener = { success, message -> + assertThat(success, `is`(true)) + }, + dataUploadService = mockedDataUploadService, + configDownloadService = mockedControlPlane + ) + someAnalytics.shutdown() + //we try pushing in events + events.forEach { + someAnalytics.track(it) + } + //all data should have been rejected + assertThat( + storage.getDataSync(), allOf( + iterableWithSize(0) + ) + ) + } + + + @Test + fun `test custom plugin`() { + println("running test test custom plugin") + val isDone = AtomicBoolean(false) + val customPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + println("inside custom plugin") + isDone.set(true) + return chain.proceed(chain.message()) + } + } + + val waitUntil = AtomicBoolean(false) + analytics.addCallback(object : Callback { + override fun success(message: Message?) { + analytics.removeCallback(this) + waitUntil.set(message is TrackMessage && message.eventName == "event") + } + + override fun failure(message: Message?, throwable: Throwable?) { + analytics.removeCallback(this) + waitUntil.set(message is TrackMessage && message.eventName == "event") + } + }) + analytics.addPlugin(customPlugin) + analytics.track { + event { +"event" } + //or event("event") + trackProperties { + //use any of these + +("property1" to "value1") + +mapOf("property2" to "value2") + add("property3" to "value3") + add(mapOf("property4" to "value4")) + } + userId("user_id") + rudderOptions { + customContexts("cc1", mapOf("cc_1_1" to "ccv")) + customContexts("cc2", mapOf("cc_2_1" to "ccv2")) + + externalId("ext-1", "ex1") + externalId("ext-2", "ex2") + externalId("ext-3", "ex3") + + integration("firebase", true) + integration("amplitude", false) + + } + } + val timeStarted = System.currentTimeMillis() + while (waitUntil.get().not() && !isDone.get()) { + if (System.currentTimeMillis() - timeStarted > 10000) { + assert(false) + break + } + } +// Awaitility.await().atMost(4, TimeUnit.SECONDS).untilTrue(isDone) + + } + + @Test + fun `test flush throttling`() { + println("running test test flush throttling") + analytics.shutdown() + Thread.sleep(500) + val isDone = AtomicBoolean(false) + //settings to make sure auto dump is off + + //spy upload service to check for throttling + val counter = AtomicInteger(0) //will check number of times called + val spyUploadService = mock(DataUploadService::class.java) + `when`(spyUploadService.uploadSync(any(), anyOrNull())).then { + Thread.sleep(500) + HttpResponse(200, null, null) + } + val events = (1..5).map { + TrackMessage.create("event:$it", RudderUtils.timeStamp) + } + val spyStorage = mock(Storage::class.java) + `when`(spyStorage.getDataSync(anyInt())).then { + val t = counter.incrementAndGet() + Thread.sleep(100) + assertThat(t, `is`(counter.get())) + if (counter.get() == 3) isDone.set(true) + events + } + analytics = Analytics( + writeKey, + Configuration( + jsonAdapter, + maxFlushInterval = 15_000, + flushQueueSize = 100, + + ), + storage = spyStorage, + initializationListener = { success, message -> + assertThat(success, `is`(true)) + }, + dataUploadService = mockedDataUploadService, + configDownloadService = mockedControlPlane + ) + events.forEach { + analytics.track(it) + } + Thread.sleep(1000) // let messages sink in + thread { + repeat((1..10).count()) { + analytics.flush() + } + } + thread { + repeat((1..20).count()) { + analytics.flush() + } + } + Awaitility.await().atMost(100, TimeUnit.SECONDS).untilTrue(isDone) + } + + @Test + fun `assert reset called on infrastructure plugins`() { + println("running test assert reset called on infrastructure plugins") + val infraPlugin = mock() + analytics.addInfrastructurePlugin(infraPlugin) + analytics.reset() + verify(infraPlugin, times(1)).reset() + } + + /** + * This method is used to generate the data of the given size. + * This method could be used to generate the message of required size (e.g., 32 KB). + * @param msgSizeInBytes The size of the message in bytes. + */ + private fun generateDataOfSize(msgSizeInBytes: Int): String { + return CharArray(msgSizeInBytes).apply { fill('a') }.joinToString("") + } + + /** + * This method is used to calculate the number of batches required to processes all the messages. + * + * Given that MAX_BATCH_SIZE is 500KB. + * Suppose we are sending 100 messages and each message is of around 31 KB, + * then we need to have 6 batches where each batch will have 16 messages and the last batch will have 4 messages. + * So this method will return 7 as the number of batches. + * + * @param message This is required to calculate the size of the message. + * @param totalNumberOfMessages This is the total number of messages that are to be sent. + */ + private fun calculateNumberOfBatches(message: Message?, totalNumberOfMessages: Int): Int { + val messageJSON = message?.let { + analytics.currentConfiguration?.jsonAdapter?.writeToJson( + it, + object : RudderTypeAdapter() {}) + } ?: return 0 + + val individualMessageSize = messageJSON.getUTF8Length() + if (individualMessageSize == 0) { + return 0 + } + + val messagesPerBatch = RudderUtils.MAX_BATCH_SIZE / individualMessageSize + return ceil(totalNumberOfMessages.toDouble() / messagesPerBatch).toInt() + } +} + +class GsonAnalyticsTest : AnalyticsTest() { + override val jsonAdapter: JsonAdapter + get() = GsonAdapter() + +} + +class JacksonAnalyticsTest : AnalyticsTest() { + override val jsonAdapter: JsonAdapter + get() = JacksonAdapter() + +} + +// Currently, we are not supporting Moshi adapter. +//class MoshiAnalyticsTest : AnalyticsTest() { +// override val jsonAdapter: JsonAdapter +// get() = MoshiAdapter() +// +//} + +@RunWith(Suite::class) +@Suite.SuiteClasses(/*MoshiAnalyticsTest::class, JacksonAnalyticsTest::class, */GsonAnalyticsTest::class +) +class AnalyticsTestSuite : TestSuite() {} diff --git a/core/src/test/java/com/rudderstack/core/CentralPluginChainTest.kt b/core/src/test/java/com/rudderstack/core/CentralPluginChainTest.kt new file mode 100644 index 000000000..762b1dd0a --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/CentralPluginChainTest.kt @@ -0,0 +1,146 @@ +package com.rudderstack.core + +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Testing flow of control through plugins. + * We will check if plugins order is maintained. + * Avoiding spying of objects. + * + */ +@RunWith(MockitoJUnitRunner::class) +class CentralPluginChainTest { + + @Mock + lateinit var mockPlugin1: Plugin + + @Mock + lateinit var mockPlugin2: Plugin + + @Mock + lateinit var mockDestinationPlugin: DestinationPlugin<*> + + private val mockMessage: Message = TrackMessage.create( + "ev-1", RudderUtils.timeStamp, + traits = mapOf( + "age" to 31, + "office" to "Rudderstack" + ), + externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), + customContextMap = null + ) + + private lateinit var centralPluginChain: CentralPluginChain + + @Before + fun setup() { + // Mock the behavior of plugins and destination plugin + whenever(mockPlugin1.intercept(any())).thenAnswer { + val chain = it.arguments[0] as CentralPluginChain + chain.proceed(mockMessage) + } + whenever(mockPlugin2.intercept(any())).thenAnswer { + val chain = it.arguments[0] as CentralPluginChain + chain.proceed(mockMessage) + } + whenever(mockDestinationPlugin.intercept(any())).thenAnswer { + val chain = it.arguments[0] as CentralPluginChain + chain.proceed(mockMessage) + } + // Initialize the list of plugins for testing + val plugins = listOf(mockPlugin1, mockPlugin2, mockDestinationPlugin) + centralPluginChain = CentralPluginChain(mockMessage, plugins, originalMessage = mockMessage) + } + + @Test + fun testMessage() { + assertThat(centralPluginChain.message(), equalTo(mockMessage)) + } + + @Test + fun testProceed() { + // Call the method under test + val resultMessage = centralPluginChain.proceed(mockMessage) + + // Verify interactions + val chainCaptor1 = argumentCaptor() + verify(mockPlugin1).intercept(chainCaptor1.capture()) + val chain1 = chainCaptor1.lastValue + assertThat(chain1.index, `is`(1)) + assertThat(chain1.plugins.size, `is`(3)) + assertThat(chain1.originalMessage, equalTo(mockMessage)) + + val chainCaptor2 = argumentCaptor() + verify(mockPlugin2).intercept(chainCaptor2.capture()) + val chain2 = chainCaptor2.lastValue + assertThat(chain2.index, `is`(2)) + assertThat(chain2.plugins.size, `is`(3)) + assertThat(chain2.originalMessage, equalTo(mockMessage)) + + val chainCaptor3 = argumentCaptor() + verify(mockDestinationPlugin).intercept(chainCaptor3.capture()) + val chain3 = chainCaptor3.lastValue + assertThat(chain3.index, `is`(3)) + assertThat(chain3.plugins.size, `is`(3)) + assertThat(chain3.originalMessage, equalTo(mockMessage)) + // Assert the result + assertThat(resultMessage, equalTo(mockMessage)) + } + + @Test(expected = IllegalStateException::class) + fun testProceedTwice() { + // Call the method under test twice + centralPluginChain.proceed(mockMessage) + centralPluginChain.proceed(mockMessage) + } + + @Test + fun testCentralPluginChainWithSubPlugins() { + val subPlugin1 = mock() + whenever(subPlugin1.intercept(any())).thenAnswer { + val chain = it.arguments[0] as CentralPluginChain + chain.proceed(mockMessage) + } + val subPlugin2 = mock() + // Mock the behavior of plugins and destination plugin + whenever(subPlugin2.intercept(any())).thenAnswer { + val chain = it.arguments[0] as CentralPluginChain + chain.proceed(mockMessage) + } + whenever(mockDestinationPlugin.subPlugins).thenReturn(listOf(subPlugin1, subPlugin2)) + + centralPluginChain.proceed(mockMessage) + // Verify interactions + val subChainCaptor1 = argumentCaptor() + verify(subPlugin1).intercept(subChainCaptor1.capture()) + val chain1 = subChainCaptor1.lastValue + assertThat(chain1.index, `is`(1)) // index should be 1 for sub plugins + assertThat(chain1.plugins.size, `is`(2)) //number of plugins should be 2 for sub plugins + assertThat(chain1.originalMessage, equalTo(mockMessage)) + + val subChainCaptor2 = argumentCaptor() + verify(subPlugin2).intercept(subChainCaptor2.capture()) + val chain2 = subChainCaptor2.lastValue + assertThat(chain2.index, `is`(2)) // index should be 2 for sub plugins + assertThat(chain2.plugins.size, `is`(2)) //number of plugins should be 2 for sub plugins + assertThat(chain2.originalMessage, equalTo(mockMessage)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/DummyExecutor.kt b/core/src/test/java/com/rudderstack/core/DummyExecutor.kt new file mode 100644 index 000000000..fbe74fb5c --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/DummyExecutor.kt @@ -0,0 +1,49 @@ +/* + * Creator: Debanjan Chatterjee on 12/12/23, 5:14 pm Last modified: 12/12/23, 5:14 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit + +class DummyExecutor : AbstractExecutorService() { + private var _isShutdown = false + override fun execute(command: Runnable?) { + command?.run() + } + + override fun shutdown() { + //No op + _isShutdown = true + } + + override fun shutdownNow(): MutableList { + // No op + shutdown() + return mutableListOf() + } + + override fun isShutdown(): Boolean { + return _isShutdown + } + + override fun isTerminated(): Boolean { + return _isShutdown + } + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { + return false + } +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/DummyWebService.kt b/core/src/test/java/com/rudderstack/core/DummyWebService.kt new file mode 100644 index 000000000..5cc87d0d1 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/DummyWebService.kt @@ -0,0 +1,149 @@ +/* + * Creator: Debanjan Chatterjee on 12/12/23, 5:42 pm Last modified: 12/12/23, 5:42 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import com.rudderstack.web.HttpInterceptor +import com.rudderstack.web.HttpResponse +import com.rudderstack.web.WebService +import java.util.concurrent.Callable +import java.util.concurrent.Future + +class DummyWebService : WebService { + var nextStatusCode = 200 + var nextBody: Any? = null + var nextErrorBody: String? = null + var nextError: Throwable? = null + private val dummyExecutor = DummyExecutor() + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseClass: Class + ): Future> { + val future: Future> = dummyExecutor.submit(Callable { + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + }) + return future + } + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter + ): Future> { + val future: Future> = dummyExecutor.submit(Callable { + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + }) + return future + } + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + callback: (HttpResponse) -> Unit + ) { + callback( + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + ) + } + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseClass: Class, + callback: (HttpResponse) -> Unit + ) { + callback( + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + ) + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseClass: Class, + isGzipEnabled: Boolean + ): Future> { + return dummyExecutor.submit(Callable { + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + }) + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + isGzipEnabled: Boolean + ): Future> { + return dummyExecutor.submit(Callable { + HttpResponse( + nextStatusCode, nextBody as? T?, nextErrorBody, nextError + ) + }) + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseClass: Class, + isGzipEnabled: Boolean, + callback: (HttpResponse) -> Unit + ) { + + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + isGzipEnabled: Boolean, + callback: (HttpResponse) -> Unit + ) { + + } + + override fun setInterceptor(httpInterceptor: HttpInterceptor) { + + } + + override fun shutdown(shutdownExecutor: Boolean) { + + } + +} \ No newline at end of file diff --git a/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ExampleUnitTest.kt b/core/src/test/java/com/rudderstack/core/ExampleUnitTest.kt similarity index 87% rename from moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ExampleUnitTest.kt rename to core/src/test/java/com/rudderstack/core/ExampleUnitTest.kt index 363f95cb8..658aaa439 100644 --- a/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ExampleUnitTest.kt +++ b/core/src/test/java/com/rudderstack/core/ExampleUnitTest.kt @@ -1,5 +1,5 @@ /* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM + * Creator: Debanjan Chatterjee on 18/10/21, 12:09 PM Last modified: 18/10/21, 12:09 PM * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,7 +12,7 @@ * permissions and limitations under the License. */ -package com.rudderstack.moshirudderadapter +package com.rudderstack.core import org.junit.Test diff --git a/core/src/test/java/com/rudderstack/core/RetryStrategyTest.kt b/core/src/test/java/com/rudderstack/core/RetryStrategyTest.kt new file mode 100644 index 000000000..8dbeaad42 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/RetryStrategyTest.kt @@ -0,0 +1,151 @@ +/* + * Creator: Debanjan Chatterjee on 04/04/22, 5:10 PM Last modified: 04/04/22, 5:10 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.equalTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class RetryStrategyTest { + + private lateinit var analytics: Analytics + + @Before + fun setup() { + analytics = generateTestAnalytics(mock()) + } + + @After + fun tearDown() { + analytics.shutdown() + } + + @Test + fun `exponential strategy succeeds on first attempt`() { + val work: Analytics.() -> Boolean = { true } + val listener = mock<(Boolean) -> Unit>() + val strategy = RetryStrategy.exponential(maxAttempts = 5) + with(strategy) { + analytics.perform(work, listener) + } + busyWait(100) + verify(listener).invoke(eq(true)) + } + + // Test ExponentialRetryStrategy with a Failing Work + @Test + fun `exponential strategy fails when work always fails`() { + val counter = AtomicInteger(0) + val work: Analytics.() -> Boolean = { + // to simulate a failing work + counter.getAndIncrement() == Int.MAX_VALUE // value is always false + } + val listener = mock<(Boolean) -> Unit>() + val strategy = RetryStrategy.exponential(maxAttempts = 3) + + with(strategy) { + analytics.perform(work, listener) + } + Awaitility.await().atMost(4500, TimeUnit.MILLISECONDS).untilAtomic(counter, equalTo(3)) + verify(listener).invoke(eq(false)) + } + + @Test + fun `test exponential retry strategy success on nth attempt`() { + val successOn = 3 //success on 3rd try + val retryCount = AtomicInteger(0) + val expectedTimeToCall = 1000L /*first*/ + 2000L /*second*/ + 4000L /*third*/ + val listener = mock<(Boolean) -> Unit>() + with(RetryStrategy.exponential()) { + analytics.perform({ + retryCount.incrementAndGet() == successOn + }, listener) + } + busyWait(expectedTimeToCall + 100) + verify(listener).invoke(eq(true)) + assertThat(retryCount.get(), equalTo(successOn)) + + } + + // Test ExponentialRetryStrategy with Limited Attempts + @Test + fun `exponential strategy succeeds before reaching max attempts`() { + val counter = AtomicInteger(0) + val work: Analytics.() -> Boolean = { counter.incrementAndGet() == 2 } + val listener = mock<(Boolean) -> Unit>() + val strategy = RetryStrategy.exponential(maxAttempts = 5) + with(strategy) { + analytics.perform(work, listener) + } + Awaitility.await().atMost(3500, TimeUnit.MILLISECONDS).untilAtomic(counter, equalTo(2)) + + verify(listener).invoke(eq(true)) + } + + @Test + fun `once canceled there should be no more attempts`() { + val counter = AtomicInteger(0) + val work: Analytics.() -> Boolean = { + counter.incrementAndGet() + false + } + val listener = mock<(Boolean) -> Unit>() + val strategy = RetryStrategy.exponential(maxAttempts = 10) + val job = with(strategy) { + analytics.perform(work, listener) + } + Awaitility.await().atMost(3500, TimeUnit.MILLISECONDS).untilAtomic(counter, equalTo(2)) + job.cancel() + busyWait(4000) + assertThat(counter.get(), Matchers.lessThanOrEqualTo(3)) + verify(listener).invoke(eq(false)) + } + // Test ExponentialRetryStrategy with Maximum Attempts Reached + + @Test + fun `once success there should be no more attempts`() { + val counter = AtomicInteger(0) + val work: Analytics.() -> Boolean = { + counter.incrementAndGet() + true + } + val listener = mock<(Boolean) -> Unit>() + val strategy = RetryStrategy.exponential(maxAttempts = 10) + val job = with(strategy) { + analytics.perform(work, listener) + } + busyWait(500) + assertThat(job.isDone(), equalTo(true)) +// while (!job.isDone()){ +// } + assertThat(counter.get(), Matchers.equalTo(1)) + busyWait(1500) // if it is not done by now, it will never be + verify(listener, times(1)).invoke(eq(true)) + } + + +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/RudderOptionTest.kt b/core/src/test/java/com/rudderstack/core/RudderOptionTest.kt new file mode 100644 index 000000000..a1a98696d --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/RudderOptionTest.kt @@ -0,0 +1,51 @@ +package com.rudderstack.core + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class RudderOptionTest { + @Test + fun `given all the externalIds are of different type, when externalIds are passed, they should be added to the list`() { + val rudderOption = RudderOption().apply { + putExternalId("brazeExternalID", "12345") + putExternalId("amplitudeExternalID", "67890") + } + + val expectedExternalIds = listOf( + mapOf("type" to "brazeExternalID", "id" to "12345"), + mapOf("type" to "amplitudeExternalID", "id" to "67890"), + ) + + assertEquals(expectedExternalIds, rudderOption.externalIds) + } + + @Test + fun `given few externalIds have same type but different Ids, when externalIds are passed, they should be merged in the list`() { + val rudderOption = RudderOption().apply { + putExternalId("brazeExternalID", "12345") + putExternalId("brazeExternalID", "67890") + putExternalId("amplitudeExternalID", "67890") + } + + val expectedExternalIds = listOf( + mapOf("type" to "brazeExternalID", "id" to "67890"), + mapOf("type" to "amplitudeExternalID", "id" to "67890"), + ) + + assertEquals(expectedExternalIds, rudderOption.externalIds) + } + + @Test + fun `given few externalIds have same type and Ids, when externalIds are passed, they should be merged in the list`() { + val rudderOption = RudderOption().apply { + putExternalId("brazeExternalID", "12345") + putExternalId("brazeExternalID", "12345") + } + + val expectedExternalIds = listOf( + mapOf("type" to "brazeExternalID", "id" to "12345"), + ) + + assertEquals(expectedExternalIds, rudderOption.externalIds) + } +} diff --git a/core/src/test/java/com/rudderstack/core/StateTest.kt b/core/src/test/java/com/rudderstack/core/StateTest.kt new file mode 100644 index 000000000..e5240ef9f --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/StateTest.kt @@ -0,0 +1,97 @@ +/* + * Creator: Debanjan Chatterjee on 04/04/22, 1:38 PM Last modified: 31/12/21, 11:32 AM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.Test + +class StateTest { + + companion object { + private const val INITIAL = "initial" + } + + //a dummy State + internal object DummyState : State(INITIAL) { + } + + /** + * Test if observers work as expected + * + */ + @Test + fun testStateObservers() { + val firstMessage = "first" + val secondMessage = "second" + val thirdMessage = "third" + + var isObserver3Called = false + var isObserver2Called = false + var isObserver1Called = false + + + //should get only first message + val observer1 = State.Observer { state, prevState -> + assertThat(state, anyOf(equalTo(INITIAL), equalTo(firstMessage))) + isObserver1Called = true + } + //should get only first and second messages + val observer2 = State.Observer { state, prevState -> + assertThat( + state, + anyOf(equalTo(INITIAL), equalTo(firstMessage), equalTo(secondMessage)) + ) + isObserver2Called = true + } + //should get first, second and third messages + val observer3 = State.Observer { state, prevState -> + assertThat( + state, anyOf( + equalTo(INITIAL), + equalTo(firstMessage), + equalTo(secondMessage), equalTo(thirdMessage), emptyOrNullString() + ) + ) + //this observer is to be called always + isObserver3Called = true + } + + DummyState.subscribe(observer1) + observer2?.apply { + DummyState.subscribe(this) + } + DummyState.subscribe(observer3) + + //test + DummyState.update(firstMessage) + assertThat(isObserver3Called, equalTo(true)) + assertThat(isObserver2Called, equalTo(true)) + assertThat(isObserver1Called, equalTo(true)) + + //remove first + DummyState.removeObserver(observer1) + + DummyState.update(secondMessage) + assertThat(isObserver3Called, equalTo(true)) + assertThat(isObserver2Called, equalTo(true)) + + DummyState.removeObserver(observer2) + DummyState.update(thirdMessage) + assertThat(isObserver3Called, equalTo(true)) + + DummyState.update(null) + } +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/TestUtils.kt b/core/src/test/java/com/rudderstack/core/TestUtils.kt new file mode 100644 index 000000000..38a46741b --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/TestUtils.kt @@ -0,0 +1,63 @@ +/* + * Creator: Debanjan Chatterjee on 04/04/22, 2:00 PM Last modified: 04/04/22, 2:00 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +@file:JvmName("TestUtils") + +package com.rudderstack.core + +import java.io.FileInputStream +import java.io.IOException +import java.util.* + +/** + * For running tests, one should have a "test.properties" file inside core module at the root, + * that contains the following values + * dataPlaneUrl=your_dp_url +controlPlaneUrl=your_cp_url +writeKey=your_write_key + * + */ + +private const val PROPERTIES_FILE = "test.properties" +private const val DATA_PLANE_URL_KEY = "dataPlaneUrl" +private const val CONTROL_PLANE_URL_KEY = "controlPlaneUrl" +private const val WRITE_KEY_KEY = "writeKey" + +private val properties: Map by lazy { + try { + Properties().let { + it.load(FileInputStream(PROPERTIES_FILE)) + mapOf( + DATA_PLANE_URL_KEY to it.getProperty(DATA_PLANE_URL_KEY), + CONTROL_PLANE_URL_KEY to it.getProperty(CONTROL_PLANE_URL_KEY), + WRITE_KEY_KEY to it.getProperty(WRITE_KEY_KEY), + ) + } + } catch (ex: IOException) { + println("test.properties file not present.") + mapOf() + } + +} +val dataPlaneUrl + get() = properties.getOrDefault(DATA_PLANE_URL_KEY, "") +val controlPlaneUrl + get() = properties.getOrDefault(CONTROL_PLANE_URL_KEY, "") +val writeKey + get() = properties.getOrDefault(WRITE_KEY_KEY, "") +fun busyWait(millis: Long) { + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < millis) { + // busy wait + } +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/compat/AnalyticsBuilderCompatTest.java b/core/src/test/java/com/rudderstack/core/compat/AnalyticsBuilderCompatTest.java new file mode 100644 index 000000000..b93acf297 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/compat/AnalyticsBuilderCompatTest.java @@ -0,0 +1,73 @@ +/* + * Creator: Debanjan Chatterjee on 08/12/23, 6:59 pm Last modified: 08/12/23, 6:59 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.compat; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +import com.rudderstack.core.Analytics; +import com.rudderstack.core.ConfigDownloadService; +import com.rudderstack.core.DataUploadService; +import com.rudderstack.jacksonrudderadapter.JacksonAdapter; +import com.rudderstack.rudderjsonadapter.JsonAdapter; + +import org.junit.Test; + +public class AnalyticsBuilderCompatTest { + private JsonAdapter jsonAdapter = new JacksonAdapter(); + @Test + public void constructorInitialization() { + AnalyticsBuilderCompat analyticsBuilder = new AnalyticsBuilderCompat("writeKey", + new ConfigurationBuilder(jsonAdapter).build()); + Analytics analytics = analyticsBuilder.build(); + assertNotNull(analytics.getCurrentConfiguration()); + assertNotNull(analytics.getDataUploadService()); + assertNotNull(analytics.getConfigDownloadService()); + } + + @Test + public void withDataUploadService() { + DataUploadService mockDataUploadService = mock(DataUploadService.class); + AnalyticsBuilderCompat analyticsBuilder = new AnalyticsBuilderCompat("writeKey", + new ConfigurationBuilder(jsonAdapter).build()) + .withDataUploadService(mockDataUploadService); + + assertEquals(mockDataUploadService, analyticsBuilder.build().getDataUploadService()); + } + + @Test + public void buildWithCustomImplementations() { + DataUploadService mockDataUploadService = mock(DataUploadService.class); + ConfigDownloadService mockConfigDownloadService = mock(ConfigDownloadService.class); + AnalyticsBuilderCompat.ShutdownHook mockShutdownHook = mock(AnalyticsBuilderCompat.ShutdownHook.class); + AnalyticsBuilderCompat.InitializationListener mockInitializationListener = + mock(AnalyticsBuilderCompat.InitializationListener.class); + + AnalyticsBuilderCompat analyticsBuilder = new AnalyticsBuilderCompat("writeKey", + new ConfigurationBuilder(jsonAdapter).build()) + .withDataUploadService(mockDataUploadService) + .withConfigDownloadService(mockConfigDownloadService) + .withShutdownHook(mockShutdownHook) + .withInitializationListener(mockInitializationListener); + + Analytics analytics = analyticsBuilder.build(); + + assertNotNull(analytics); + assertEquals(mockDataUploadService, analytics.getDataUploadService()); + assertEquals(mockConfigDownloadService, analytics.getConfigDownloadService()); + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/compat/ConfigurationBuilderTest.java b/core/src/test/java/com/rudderstack/core/compat/ConfigurationBuilderTest.java new file mode 100644 index 000000000..9bfd67fc7 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/compat/ConfigurationBuilderTest.java @@ -0,0 +1,119 @@ +/* + * Creator: Debanjan Chatterjee on 08/12/23, 8:01 pm Last modified: 08/12/23, 8:01 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.compat; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +import com.rudderstack.core.Base64Generator; +import com.rudderstack.core.Configuration; +import com.rudderstack.core.Logger; +import com.rudderstack.core.RetryStrategy; +import com.rudderstack.core.RudderOption; +import com.rudderstack.core.Storage; +import com.rudderstack.rudderjsonadapter.JsonAdapter; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ConfigurationBuilderTest { + + @Test + public void buildConfigurationWithDefaultValues() { + JsonAdapter mockJsonAdapter = mock(JsonAdapter.class); + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(mockJsonAdapter); + + Configuration configuration = configurationBuilder.build(); + + assertNotNull(configuration); + assertEquals(mockJsonAdapter, configuration.getJsonAdapter()); + assertEquals(new RudderOption(), configuration.getOptions()); + assertEquals(Configuration.FLUSH_QUEUE_SIZE, configuration.getFlushQueueSize()); + assertEquals(Configuration.MAX_FLUSH_INTERVAL, configuration.getMaxFlushInterval()); + assertFalse(configuration.getShouldVerifySdk()); + assertThat(configuration.getSdkVerifyRetryStrategy(), + Matchers.isA(RetryStrategy.ExponentialRetryStrategy.class)); + assertThat(configuration.getDataPlaneUrl(), equalTo("https://hosted.rudderlabs.com")); + assertNotNull(configuration.getLogger()); + assertNotNull(configuration.getAnalyticsExecutor()); + assertNotNull(configuration.getNetworkExecutor()); + assertNotNull(configuration.getBase64Generator()); + } + + @Test + public void buildConfigurationWithCustomValues() { + JsonAdapter mockJsonAdapter = mock(JsonAdapter.class); + RudderOption customOptions = new RudderOption(); + int customFlushQueueSize = 100; + long customMaxFlushInterval = 5000; + boolean customShouldVerifySdk = true; + RetryStrategy customRetryStrategy = RetryStrategy.exponential(); + String customDataPlaneUrl = "https://custom-data-plane-url.com"; + String customControlPlaneUrl = "https://custom-control-plane-url.com"; + Storage customStorage = mock(Storage.class); + ExecutorService customAnalyticsExecutor = Executors.newFixedThreadPool(2); + ExecutorService customNetworkExecutor = Executors.newFixedThreadPool(3); + Base64Generator customBase64Generator = mock(Base64Generator.class); + + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(mockJsonAdapter) + .withOptions(customOptions) + .withFlushQueueSize(customFlushQueueSize) + .withMaxFlushInterval(customMaxFlushInterval) + .withShouldVerifySdk(customShouldVerifySdk) + .withSdkVerifyRetryStrategy(customRetryStrategy) + .withDataPlaneUrl(customDataPlaneUrl) + .withControlPlaneUrl(customControlPlaneUrl) + .withAnalyticsExecutor(customAnalyticsExecutor) + .withNetworkExecutor(customNetworkExecutor) + .withBase64Generator(customBase64Generator); + + Configuration configuration = configurationBuilder.build(); + + assertNotNull(configuration); + assertEquals(mockJsonAdapter, configuration.getJsonAdapter()); + assertEquals(customOptions, configuration.getOptions()); + assertEquals(customFlushQueueSize, configuration.getFlushQueueSize()); + assertEquals(customMaxFlushInterval, configuration.getMaxFlushInterval()); + assertEquals(customShouldVerifySdk, configuration.getShouldVerifySdk()); + assertEquals(customRetryStrategy, configuration.getSdkVerifyRetryStrategy()); + assertEquals(customDataPlaneUrl, configuration.getDataPlaneUrl()); + assertEquals(customControlPlaneUrl, configuration.getControlPlaneUrl()); + assertEquals(customAnalyticsExecutor, configuration.getAnalyticsExecutor()); + assertEquals(customNetworkExecutor, configuration.getNetworkExecutor()); + assertEquals(customBase64Generator, configuration.getBase64Generator()); + } + + @Test + public void when_logLevel_DEBUG_is_passed_then_assert_that_configuration_has_this_logLevel_set_as_a_property() { + JsonAdapter mockJsonAdapter = mock(JsonAdapter.class); + + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(mockJsonAdapter) + .withLogLevel(Logger.LogLevel.DEBUG); + + Configuration configuration = configurationBuilder.build(); + + assertNotNull(configuration); + assertEquals(Logger.LogLevel.DEBUG, configuration.getLogger().getLevel()); + } + + // Add more test cases as needed for edge cases, validation, etc. +} diff --git a/core/src/test/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicyTest.kt b/core/src/test/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicyTest.kt new file mode 100644 index 000000000..5a2b4b85c --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/flushpolicy/CountBasedFlushPolicyTest.kt @@ -0,0 +1,76 @@ +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.busyWait +import com.rudderstack.core.models.Message +import com.rudderstack.web.HttpResponse +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.concurrent.atomic.AtomicInteger + +class CountBasedFlushPolicyTest { + private lateinit var analytics: Analytics + private lateinit var flushPolicy: CountBasedFlushPolicy + private lateinit var mockUploadService: DataUploadService + + @Before + fun setup() { + mockUploadService = mock() + val mockedResponse: HttpResponse = HttpResponse(200, "OK", null) + whenever(mockUploadService.upload(any(), any(), any())).then { +// storage.deleteMessages(data) + it.getArgument<(response: HttpResponse) -> Unit>(2).invoke( + mockedResponse + ) + } + Mockito.`when`(mockUploadService.uploadSync(any>(), anyOrNull())).thenReturn( + mockedResponse + ) + analytics = generateTestAnalytics( + Configuration(mock(), shouldVerifySdk = false), dataUploadService = mockUploadService + ) + flushPolicy = CountBasedFlushPolicy() + analytics.removeAllFlushPolicies() + analytics.addFlushPolicies(flushPolicy) + } + + @After + fun tearDown() { + flushPolicy.shutdown() + analytics.shutdown() + } + + @Test + fun testSetup() { + flushPolicy.setup(analytics) + //test do not crash + } + + @Test + fun testUpdateConfiguration() { + val config = mock() + val flushCalledCount = AtomicInteger(0) + whenever(config.maxFlushInterval).thenReturn(100000) + whenever(config.flushQueueSize).thenReturn(1) + analytics.track { event("event") } + flushPolicy.setFlush { + flushCalledCount.incrementAndGet() + } + flushPolicy.updateConfiguration(config) + + busyWait(250L) + MatcherAssert.assertThat(flushCalledCount.get(), Matchers.equalTo(1)) + } + +} diff --git a/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyExtensionTests.kt b/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyExtensionTests.kt new file mode 100644 index 000000000..60f7d13be --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyExtensionTests.kt @@ -0,0 +1,85 @@ +/* + * Creator: Debanjan Chatterjee on 08/02/24, 7:29 pm Last modified: 08/02/24, 7:29 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Controller +import com.rudderstack.core.InfrastructurePlugin +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(MockitoJUnitRunner::class) + +class FlushPolicyExtensionTests { + + @Mock + private lateinit var controller: Controller + + @Mock + private lateinit var flushPolicy1: FlushPolicy + + @Mock + private lateinit var flushPolicy2: FlushPolicy + + @Test + fun testAddFlushPolicies() { + controller.addFlushPolicies(flushPolicy1, flushPolicy2) + verify(controller).addInfrastructurePlugin(flushPolicy1, flushPolicy2) + } + + @Test + fun testSetFlushPolicies() { + + whenever(controller.applyInfrastructureClosure(any())).thenAnswer { + val closure = it.getArgument<(InfrastructurePlugin.() -> Unit)>(0) + closure(flushPolicy1) + closure(flushPolicy2) + } + controller.setFlushPolicies(flushPolicy1, flushPolicy2) + verify(controller).removeInfrastructurePlugin(flushPolicy1) + verify(controller).removeInfrastructurePlugin(flushPolicy2) + verify(controller).addFlushPolicies(flushPolicy1, flushPolicy2) + } + + @Test + fun testRemoveAllFlushPolicies() { + whenever(controller.applyInfrastructureClosure(any())).thenAnswer { + val closure = it.getArgument<(InfrastructurePlugin.() -> Unit)>(0) + closure(flushPolicy1) + closure(flushPolicy2) + } + controller.removeAllFlushPolicies() + verify(controller).applyInfrastructureClosure(any()) + verify(controller).removeInfrastructurePlugin(flushPolicy1) + verify(controller).removeInfrastructurePlugin(flushPolicy2) + } + + @Test + fun testRemoveFlushPolicy() { + controller.removeFlushPolicy(flushPolicy1) + verify(controller).removeInfrastructurePlugin(flushPolicy1) + } + + @Test + fun testApplyFlushPoliciesClosure() { + controller.applyFlushPoliciesClosure { onRemoved() } + verify(controller).applyInfrastructureClosure(any()) + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyTest.kt b/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyTest.kt new file mode 100644 index 000000000..e437cc41e --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/flushpolicy/FlushPolicyTest.kt @@ -0,0 +1,45 @@ +/* + * Creator: Debanjan Chatterjee on 08/02/24, 12:04 pm Last modified: 08/02/24, 12:04 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class FlushPolicyTest { + private lateinit var analytics : Analytics + private lateinit var flushPolicy : FlushPolicy + @Before + fun setup() { + analytics = generateTestAnalytics(mock()) + flushPolicy = mock() + } + @After + fun destroy() { + analytics.shutdown() + } + @Test + fun testRescheduleCalledOnFlush() { + analytics.addFlushPolicies(flushPolicy) + analytics.blockingFlush() + verify(flushPolicy).reschedule() + //test do not crash + } +} \ No newline at end of file diff --git a/core/src/test/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicyTest.kt b/core/src/test/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicyTest.kt new file mode 100644 index 000000000..1e2fad867 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/flushpolicy/IntervalBasedFlushPolicyTest.kt @@ -0,0 +1,90 @@ +package com.rudderstack.core.flushpolicy + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.busyWait +import com.rudderstack.core.models.Message +import com.rudderstack.web.HttpResponse +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.concurrent.atomic.AtomicInteger + +class IntervalBasedFlushPolicyTest { + private lateinit var analytics: Analytics + private lateinit var flushPolicy: IntervalBasedFlushPolicy + private lateinit var mockUploadService: DataUploadService + + @Before + fun setup() { + mockUploadService = mock() + val mockedResponse: HttpResponse = HttpResponse(200, "OK", null) + whenever(mockUploadService.upload(any(), any(), any())).then { +// storage.deleteMessages(data) + it.getArgument<(response: HttpResponse) -> Unit>(2).invoke( + mockedResponse + ) + } + Mockito.`when`(mockUploadService.uploadSync(any>(), anyOrNull())).thenReturn( + mockedResponse + ) + analytics = generateTestAnalytics( + Configuration(mock(), shouldVerifySdk = false), + dataUploadService = mockUploadService + ) + flushPolicy = IntervalBasedFlushPolicy() + analytics.removeAllFlushPolicies() + analytics.addFlushPolicies(flushPolicy) + } + + @After + fun tearDown() { + flushPolicy.shutdown() + analytics.shutdown() + } + + @Test + fun testSetup() { + flushPolicy.setup(analytics) + //test do not crash + } + + @Test + fun testUpdateConfiguration() { + val config = mock() + val flushCalledCount = AtomicInteger(0) + whenever(config.maxFlushInterval).thenReturn(100) + flushPolicy.setFlush { + flushCalledCount.incrementAndGet() + } + flushPolicy.updateConfiguration(config) + busyWait(150L) + assertThat(flushCalledCount.get(), Matchers.equalTo(1)) + } + + @Test + fun testReschedule() { + val config = mock() + val flushCalledCount = AtomicInteger(0) + whenever(config.maxFlushInterval).thenReturn(300) + flushPolicy.setFlush { + flushCalledCount.incrementAndGet() + } + + flushPolicy.updateConfiguration(config)// a long duration + busyWait(250) // just before the flush + flushPolicy.reschedule() + + busyWait(150L) + assertThat(flushCalledCount.get(), Matchers.equalTo(0)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/holder/AnalyticsHolderTest.kt b/core/src/test/java/com/rudderstack/core/holder/AnalyticsHolderTest.kt new file mode 100644 index 000000000..08687d99f --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/holder/AnalyticsHolderTest.kt @@ -0,0 +1,84 @@ +/* + * Creator: Debanjan Chatterjee on 08/02/24, 10:10 am Last modified: 08/02/24, 10:09 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.holder + +import com.rudderstack.core.Controller +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(MockitoJUnitRunner::class) +class AnalyticsHolderTest { + @Mock + private lateinit var controller: Controller + + @Before + fun setup() { + whenever(controller.writeKey).thenReturn("writeKey") + } + @After + fun tearDown() { + controller.clearAll() + } + @Test + fun testStoreAndRetrieve() { + // Setup + val identifier = "testIdentifier" + val value = "testValue" + + // Execute + controller.store(identifier, value) + + // Verify + assertEquals(value, controller.retrieve(identifier)) + } + + @Test + fun testRemove() { + // Setup + val identifier = "testIdentifier" + val value = "testValue" + controller.store(identifier, value) + // Execute + controller.remove(identifier) + + // Verify + assertNull(controller.retrieve(identifier)) + } + + @Test + fun testMultipleControllerAccess() { + // Setup + val identifier = "testIdentifier" + val valueForController1 = "testValue" + val valueForController2 = "testValue2" + val controller2 = mock() + whenever(controller2.writeKey).thenReturn("writeKey2") + + // Execute + controller.store(identifier, valueForController1) + controller2.store(identifier, valueForController2) + + // Verify + assertEquals(valueForController1, controller.retrieve(identifier)) + assertEquals(valueForController2, controller2.retrieve(identifier)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/holder/StatesHolderTest.kt b/core/src/test/java/com/rudderstack/core/holder/StatesHolderTest.kt new file mode 100644 index 000000000..d6911cd1e --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/holder/StatesHolderTest.kt @@ -0,0 +1,79 @@ +/* + * Creator: Debanjan Chatterjee on 08/02/24, 10:12 am Last modified: 08/02/24, 10:12 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.core.holder + +import com.rudderstack.core.Controller +import com.rudderstack.core.State +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.isNull +import org.mockito.kotlin.whenever + +@RunWith(MockitoJUnitRunner::class) +class StatesHolderTest { + @Mock + private lateinit var controller: Controller + + @Before + fun setup() { + whenever(controller.writeKey).thenReturn("testInstance") + } + @After + fun tearDown() { + controller.clearAll() + } + class TestState : State() + @Test + fun testAssociateAndRetrieveState() { + // Setup + val state = TestState() + + // Execute + controller.associateState(state) + + // Verify + assertThat(controller.retrieveState(), Matchers.equalTo(state)) + } + @Test + fun testLastAssociatedStateCanOnlyBeRetrieved() { + // Setup + val state1 = TestState() + val state2 = TestState() + + // Execute + controller.associateState(state1) + controller.associateState(state2) + + // Verify + assertThat(controller.retrieveState(), Matchers.equalTo(state2)) + } + @Test + fun testStateRemoval() { + // Setup + val state = TestState() + // Execute + controller.associateState(state) + controller.removeState() + + // Verify + assertThat(controller.retrieveState(), Matchers.nullValue()) + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/ConfigDownloadServiceImplTest.kt b/core/src/test/java/com/rudderstack/core/internal/ConfigDownloadServiceImplTest.kt new file mode 100644 index 000000000..02f203cb9 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/ConfigDownloadServiceImplTest.kt @@ -0,0 +1,190 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.DummyWebService +import com.rudderstack.core.RetryStrategy +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.writeKey +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.moshirudderadapter.MoshiAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.vagabond.testcommon.generateTestAnalytics +import junit.framework.TestSuite +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import java.util.Base64 +import java.util.Locale +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +abstract class ConfigDownloadServiceImplTest { + protected abstract val jsonAdapter: JsonAdapter + private lateinit var configDownloadServiceImpl: ConfigDownloadServiceImpl + private lateinit var dummyWebService: DummyWebService + private lateinit var analytics: Analytics + + @Before + fun setup() { + dummyWebService = DummyWebService() + val config = Configuration( + jsonAdapter = jsonAdapter, + sdkVerifyRetryStrategy = RetryStrategy.exponential(1) + ) + analytics = generateTestAnalytics(config) + dummyWebService.nextBody = RudderServerConfig(source = RudderServerConfig.RudderServerConfigSource()) + configDownloadServiceImpl = ConfigDownloadServiceImpl( + Base64.getEncoder().encodeToString( + String.format(Locale.US, "%s:", writeKey).toByteArray(charset("UTF-8")) + ), dummyWebService + ) + configDownloadServiceImpl.setup(analytics) + configDownloadServiceImpl.updateConfiguration(config) + } + + @Test + fun `test successful config download`() { + val isComplete = AtomicBoolean(false) + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + assertThat(success, `is`(true)) + assertThat(lastErrorMsg, nullValue()) + assertThat(rudderServerConfig, Matchers.notNullValue()) + assertThat(rudderServerConfig?.source, Matchers.notNullValue()) + isComplete.set(true) + }) + + Awaitility.await().atMost(1, TimeUnit.SECONDS).untilTrue(isComplete) + } + + @Test + fun `test failure config download`() { + val isComplete = AtomicBoolean(false) + dummyWebService.nextStatusCode = 400 + dummyWebService.nextBody = null + dummyWebService.nextErrorBody = "Bad Request" + analytics.currentConfiguration?.let { + configDownloadServiceImpl.updateConfiguration( + it + ) + } + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + assertThat(success, `is`(false)) + assertThat(lastErrorMsg, notNullValue()) + assertThat(rudderServerConfig, nullValue()) + isComplete.set(true) + }) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilTrue(isComplete) + } + + @Test + fun `test listener is fired when download is called`() { + val mockListener = mock() + val isComplete = AtomicBoolean(false) + configDownloadServiceImpl.addListener(mockListener, 0) + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + isComplete.set(true) + }) + while (!isComplete.get()) { + } + verify(mockListener, times(1)).onDownloaded(true) + } + + @Test + fun `test listener is fired when download replayed`() { + val mockListener = mock() + val isComplete = AtomicBoolean(false) + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + isComplete.set(true) + }) + while (!isComplete.get()) { + } + isComplete.set(false) + configDownloadServiceImpl.updateConfiguration( + Configuration( + jsonAdapter = jsonAdapter, + sdkVerifyRetryStrategy = RetryStrategy.exponential(0) + ) + ) + dummyWebService.nextStatusCode = 400 + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + isComplete.set(true) + }) + while (!isComplete.get()) { + } + configDownloadServiceImpl.addListener(mockListener, 1) + verify(mockListener, times(1)).onDownloaded(false) + } + + @Test + fun `test listener is not fired when attached post download and replay is 0`() { + val mockListener = mock() + val isComplete = AtomicBoolean(false) + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + isComplete.set(true) + }) + while (!isComplete.get()) { + } + configDownloadServiceImpl.addListener(mockListener, 0) + verify(mockListener, never()).onDownloaded(org.mockito.kotlin.any()) + } + + @Test + fun `test listener removed wont trigger onDownloaded`() { + val mockListener = mock() + val isComplete = AtomicBoolean(false) + configDownloadServiceImpl.addListener(mockListener, 0) + configDownloadServiceImpl.removeListener(mockListener) + configDownloadServiceImpl.download(callback = { success, rudderServerConfig, lastErrorMsg -> + isComplete.set(true) + }) + while (!isComplete.get()) { + } + verify(mockListener, never()).onDownloaded(org.mockito.kotlin.any()) + } + + @After + fun destroy() { + configDownloadServiceImpl.shutdown() + analytics.shutdown() + } + +} + +class ConfigDownloadTestWithJackson : ConfigDownloadServiceImplTest() { + override val jsonAdapter: JsonAdapter = JacksonAdapter() + +} + +class ConfigDownloadTestWithGson : ConfigDownloadServiceImplTest() { + override val jsonAdapter: JsonAdapter = GsonAdapter() + +} + +class ConfigDownloadTestWithMoshi : ConfigDownloadServiceImplTest() { + override val jsonAdapter: JsonAdapter = MoshiAdapter() + +} + +@RunWith(Suite::class) +@Suite.SuiteClasses( + ConfigDownloadTestWithMoshi::class, + ConfigDownloadTestWithGson::class, + ConfigDownloadTestWithJackson::class +) +class ConfigDownloadTestSuite : TestSuite() diff --git a/core/src/test/java/com/rudderstack/core/internal/DataServiceUploadImplTest.kt b/core/src/test/java/com/rudderstack/core/internal/DataServiceUploadImplTest.kt new file mode 100644 index 000000000..50c9d4294 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/DataServiceUploadImplTest.kt @@ -0,0 +1,179 @@ +package com.rudderstack.core.internal + +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.dataPlaneUrl +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.moshirudderadapter.MoshiAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.web.HttpResponse +import com.rudderstack.web.WebService +import junit.framework.TestSuite +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.emptyString +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.greaterThanOrEqualTo +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.lessThan +import org.hamcrest.Matchers.not +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyMap +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import java.util.Base64 +import java.util.Locale + +abstract class DataServiceUploadImplTest { + protected abstract val jsonAdapter: JsonAdapter + private lateinit var dataServiceImpl: DataUploadService + + // private val dummyWebService = DummyWebService() + private val testMessagesList = listOf( + TrackMessage.create("m-1", anonymousId = "anon-1", timestamp = "09-01-2022"), + TrackMessage.create("m-2", anonymousId = "anon-2", timestamp = "09-01-2022"), + TrackMessage.create("m-3", anonymousId = "anon-3", timestamp = "09-01-2022"), + TrackMessage.create("m-4", anonymousId = "anon-4", timestamp = "09-01-2022"), + TrackMessage.create("m-5", anonymousId = "anon-5", timestamp = "09-01-2022"), + TrackMessage.create("m-6", anonymousId = "anon-6", timestamp = "09-01-2022"), + TrackMessage.create("m-7", anonymousId = "anon-7", timestamp = "09-01-2022"), + TrackMessage.create("m-8", anonymousId = "anon-8", timestamp = "09-01-2022"), + TrackMessage.create("m-9", anonymousId = "anon-9", timestamp = "09-01-2022"), + TrackMessage.create("m-10", anonymousId = "anon-10", timestamp = "09-01-2022"), + TrackMessage.create("m-11", anonymousId = "anon-11", timestamp = "09-01-2022"), + ) + + @Before + fun setup() { + + } + + + @Test + fun `test proper data sent to web service`() { + val writeKey = "write_key" + val dummyWebService = mock { + on { + post( + anyMap(), + anyOrNull(), + anyString(), + anyString(), + any>(), + anyBoolean(), + any() + ) + }.doAnswer { + val callback = it.arguments[6] as (HttpResponse) -> Unit + callback(HttpResponse(200, "OK", null)) + } + + } + dataServiceImpl = DataUploadServiceImpl( + writeKey, dummyWebService + ) + val configuration = + Configuration(jsonAdapter, dataPlaneUrl = dataPlaneUrl, base64Generator = { + Base64.getEncoder().encodeToString( + String.format(Locale.US, "%s:", it).toByteArray(charset("UTF-8")) + ) + }) + dataServiceImpl.updateConfiguration(configuration) +// val isComplete = AtomicBoolean(false) + val argCaptors = argumentCaptor, String, String, Class, Boolean>( +// Map::class, //headers +// String::class,//body +// String::class,//endpoint +// Class::class,// response class +// Boolean::class,//gzip + ) + dataServiceImpl.upload(testMessagesList) { + assertThat(it.status, allOf(greaterThanOrEqualTo(200), lessThan(209))) + } + verify(dummyWebService).post( + argCaptors.component1().capture(), + anyOrNull(), + argCaptors.component2().capture(), + argCaptors.component3().capture(), + argCaptors.component4().capture(), + argCaptors.component5().capture(), + anyOrNull() + ) + val encodedWriteKey = configuration.base64Generator?.generateBase64(writeKey) + assertThat(argCaptors.component1().lastValue, allOf(hasEntry("Content-Type", "application/json"), + hasEntry("Authorization", String.format(Locale.US, "Basic %s", encodedWriteKey)))) + assertThat(argCaptors.component2().lastValue, not(emptyString())) + assertThat(argCaptors.component3().lastValue, equalTo("v1/batch")) + assertThat(argCaptors.component4().lastValue, equalTo(String::class.java)) + assertThat(argCaptors.component5().lastValue, equalTo(configuration.gzipEnabled)) + } + + @Test + fun testUploadFailure() { + val dummyWebService = mock{ + on { + post( + anyMap(), + anyOrNull(), + anyString(), + anyString(), + any>(), + anyBoolean(), + any() + ) + }.doAnswer { + val callback = it.arguments[6] as (HttpResponse) -> Unit + callback(HttpResponse(400, "Bad Request", null)) + } + } + dataServiceImpl = DataUploadServiceImpl("write_key", dummyWebService) + dataServiceImpl.upload(testMessagesList) { + assertThat(it.status, allOf(greaterThanOrEqualTo(400))) + } + } +// +// @Test +// fun testUploadSync() { +// dummyWebService.nextStatusCode = 200 +// val response = dataServiceImpl.uploadSync(testMessagesList) +// assertThat(response?.status ?: 0, allOf(greaterThanOrEqualTo(200), lessThan(209))) +// +// } +} + +class DataServiceUploadTestWithJackson : DataServiceUploadImplTest() { + override val jsonAdapter: JsonAdapter = JacksonAdapter() + +} + +class DataServiceUploadTestWithGson : DataServiceUploadImplTest() { + override val jsonAdapter: JsonAdapter = GsonAdapter() + +} + +class DataServiceUploadTestWithMoshi : DataServiceUploadImplTest() { + override val jsonAdapter: JsonAdapter = MoshiAdapter() + +} + +@RunWith(Suite::class) +@Suite.SuiteClasses( + DataServiceUploadTestWithJackson::class, + DataServiceUploadTestWithGson::class, + DataServiceUploadTestWithMoshi::class +) +class DataUploadTestSuite : TestSuite() { + +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/CoreInputsPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/CoreInputsPluginTest.kt new file mode 100644 index 000000000..1af9669a0 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/CoreInputsPluginTest.kt @@ -0,0 +1,91 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.Storage +import com.rudderstack.core.models.TrackMessage +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class CoreInputsPluginTest { + private lateinit var analytics: Analytics + private lateinit var storage: Storage + private lateinit var coreInputsPlugin: CoreInputsPlugin + + @Before + fun setup() { + coreInputsPlugin = CoreInputsPlugin() + storage = mock() + whenever(storage.libraryName) doReturn "MyLibrary" + whenever(storage.libraryVersion) doReturn "1.0" + analytics = generateTestAnalytics(mock(), storage = storage) + } + + @After + fun shutdown() { + coreInputsPlugin.onShutDown() + storage.shutdown() + analytics.shutdown() + } + + @Test + fun `intercept method adds library context to message context when storage is not null`() { + // Arrange + val mockChain = mock() + val customContextMap = mapOf("existingKey" to "existingValue") + val mockMessage = TrackMessage.create( + "ev_name", RudderUtils.timeStamp, + customContextMap = customContextMap + ) + `when`(mockChain.message()).thenReturn(mockMessage) + whenever(mockChain.proceed(any())) doAnswer { + it.getArgument(0) + } + coreInputsPlugin.setup(analytics) + // Act + val result = coreInputsPlugin.intercept(mockChain) + + // Assert + val expectedContext = mapOf("customContextMap" to customContextMap) + mapOf( + "library" to mapOf( + "name" to "MyLibrary", + "version" to "1.0" + ) + ) + assertThat(result.context?.filterValues { it != null }, `is`(equalTo(expectedContext))) + verify(mockChain).proceed(mockMessage.copy(context = result.context)) + } + + @Test + fun `intercept method adds library context to message context when storage is not null context null`() { + // Arrange + val mockChain = mock() + val mockMessage = TrackMessage.create("ev_name", RudderUtils.timeStamp) + `when`(mockChain.message()).thenReturn(mockMessage) + whenever(mockChain.proceed(any())) doAnswer { + it.getArgument(0) + } + coreInputsPlugin.setup(analytics) + // Act + val result = coreInputsPlugin.intercept(mockChain) + + // Assert + val expectedContext = mapOf("library" to mapOf("name" to "MyLibrary", "version" to "1.0")) + assertThat(result.context?.filterValues { it != null }, `is`(equalTo(expectedContext))) + verify(mockChain).proceed(mockMessage.copy(context = result.context)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPluginTest.kt new file mode 100644 index 000000000..ab0663d6e --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/DestinationConfigurationPluginTest.kt @@ -0,0 +1,137 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.BaseDestinationPlugin +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.everyItem +import org.hamcrest.Matchers.isA +import org.hamcrest.Matchers.iterableWithSize +import org.hamcrest.Matchers.not +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * For testing [DestinationConfigurationPlugin] we will create + * destination plugins, update rudder server config and check the result + * of plugin.intercept + */ +class DestinationConfigurationPluginTest { + private val destinations = listOf( + BaseDestinationPlugin("d-1") { + return@BaseDestinationPlugin it.proceed(it.message()) + }, + BaseDestinationPlugin("d-2") { + return@BaseDestinationPlugin it.proceed(it.message()) + }, + BaseDestinationPlugin("d-3") { + return@BaseDestinationPlugin it.proceed(it.message()) + }, + BaseDestinationPlugin("d-4") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + ) + private val message = TrackMessage.create("some_event", timestamp = RudderUtils.timeStamp) + + //test plugin + private var destinationConfigurationPlugin: DestinationConfigurationPlugin? = null + + //chain to proceed with + private var defaultPluginChain: CentralPluginChain? = null + + @Before + fun setup() { + destinationConfigurationPlugin = DestinationConfigurationPlugin() + defaultPluginChain = CentralPluginChain( + message = message, + plugins = destinations, + originalMessage = message + ) + } + + @After + fun destroy() { + destinationConfigurationPlugin = null + defaultPluginChain = null + } + + @Test + fun `test destination filtering with config set`() { + //setting config + destinationConfigurationPlugin?.updateRudderServerConfig( + RudderServerConfig( + source = + RudderServerConfig.RudderServerConfigSource( + destinations = + listOf( + RudderServerConfig.RudderServerDestination( + destinationId = "1", + destinationName = "d-1", + destinationConfig = mapOf(), + isDestinationEnabled = true + ), + RudderServerConfig.RudderServerDestination( + destinationId = "2", + destinationName = "d-2", + destinationConfig = mapOf(), + isDestinationEnabled = false + ) + ) + ) + ) + ) + //adding a assertion plugin + val centralPluginChain = defaultPluginChain!!.copy( + plugins = defaultPluginChain!!.plugins.toMutableList().also { + //after destination config plugin + it.add(0, object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat( + chain.plugins, allOf( + Matchers.hasItems(*(destinations.toMutableList().also { + it.removeIf { it is DestinationPlugin<*> && it.name == "d-2" } + }.toTypedArray())), everyItem(not(destinations[1]/*isIn(shouldNotBeInList)*/)) + ) + ) + return chain.proceed(chain.message()) + } + }) + } + ) + destinationConfigurationPlugin!!.intercept(centralPluginChain) + } + + @Test + fun `test destination filtering with config not set`() { + + //adding a assertion plugin + val centralPluginChain = defaultPluginChain!!.copy( + plugins = defaultPluginChain!!.plugins.toMutableList().also { + //after destination config plugin + it.add(0, object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat( + chain.plugins, allOf( + iterableWithSize(1), + everyItem(not(isA(DestinationPlugin::class.java))) + ) + ) + return chain.proceed(chain.message()) + } + }) + } + ) + destinationConfigurationPlugin!!.intercept(centralPluginChain) + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/EventFilteringPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/EventFilteringPluginTest.kt new file mode 100644 index 000000000..b6499f75e --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/EventFilteringPluginTest.kt @@ -0,0 +1,442 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.BaseDestinationPlugin +import com.rudderstack.core.DestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.core.models.RudderServerConfig +import com.rudderstack.core.models.TrackMessage +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasProperty +import org.hamcrest.Matchers.not +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyList +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.Date + +class EventFilteringPluginTest { + // Test subject + private lateinit var eventFilteringPlugin: EventFilteringPlugin + + @Before + fun setUp() { + eventFilteringPlugin = EventFilteringPlugin() + eventFilteringPlugin.onShutDown() + eventFilteringPlugin.setup(generateTestAnalytics(JacksonAdapter())) + } + + @Test + fun testIntercept_AllowAllEventsWhenNoServerConfig() { + // Given the plugin is not updated with server config, + // any message reaching it will be forwarded without a check + val chain = mock() + val demoMessage = TrackMessage.create( + "testEvent", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + //when + eventFilteringPlugin.intercept(chain) + // same chain is used + verify(chain, times(1)).proceed(demoMessage) + } + + @Test + fun testIntercept_AllowAllEventsWhenServerConfigWithoutDestinations() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig(isHosted = true) + val demoMessage = TrackMessage.create( + "testEvent", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) +// val dummyPlugins = listOf(Plugin { it.proceed(it.message()) }, +// Plugin { it.proceed(it.message()) }) +// // When +// whenever(chain.plugins).thenReturn(dummyPlugins) + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + eventFilteringPlugin.intercept(chain) + + //Then + //no copy of chain is made since filteredEventsMap is empty + verify(chain, times(1)).proceed(demoMessage) + + } + + @Test + fun testIntercept_AllowAllEventsWhenServerConfigWithEmptyDestinations() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource(destinations = emptyList()) + ) + + // When + val demoMessage = TrackMessage.create( + "testEvent", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + eventFilteringPlugin.intercept(chain) + // Then + //no copy of chain is made since filteredEventsMap is empty + verify(chain, times(1)).proceed(demoMessage) + } + + @Test + fun `allow all destination plugins when event name not in black list`() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource( + destinations = listOf( + RudderServerConfig.RudderServerDestination( + destinationId = "test", + destinationName = "TestDestination", + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "Firebase", + ), + isDestinationEnabled = true, + updatedAt = "2022-01-01", + destinationConfig = mapOf( + "eventFilteringOption" to "blacklistedEvents", + "blacklistedEvents" to setOf( + mapOf("eventName" to "test1"), mapOf( + "eventName" to + "test2", + ), mapOf("eventName" to "test3") + ) + ) + ) + ) + ) + ) + val demoMessage = TrackMessage.create( + "testEvent", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + //destination plugin list + whenever(chain.plugins).thenReturn( + listOf( + createDummyDestinationPluginWithName("Firebase"), + createDummyDestinationPluginWithName("Mixpanel"), + ) + ) + var chainCopy: Plugin.Chain? = null + whenever(chain.with(anyList())).then { + val list = it.arguments[0] as List + CentralPluginChain(demoMessage, list, originalMessage = demoMessage).also { chainCopy = it } + } + // When + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + + eventFilteringPlugin.intercept(chain) + + // Then + verify(chain, times(1)).with(anyOrNull()) + MatcherAssert.assertThat( + chainCopy, allOf( + org.hamcrest.Matchers.notNullValue(), + org.hamcrest.Matchers.instanceOf(CentralPluginChain::class.java), + org.hamcrest.Matchers.hasProperty( + "plugins", + allOf( + Matchers.iterableWithSize(2), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Firebase")) + ) + ), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Mixpanel")) + ) + ) + ) + ) + ) + ) + //a copy of chain is made since filteredEventsMap is not empty + verify(chain, never()).proceed(demoMessage) + } + + @Test + fun `disallow destination plugins when event name in black list`() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource( + destinations = listOf( + RudderServerConfig.RudderServerDestination( + destinationId = "test", + destinationName = "TestDestination", + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "Firebase", + ), + isDestinationEnabled = true, + updatedAt = "2022-01-01", + destinationConfig = mapOf( + "eventFilteringOption" to "blacklistedEvents", + "blacklistedEvents" to setOf( + mapOf("eventName" to "test1"), mapOf( + "eventName" to + "test2", + ), mapOf("eventName" to "test3") + ) + ) + ) + ) + ) + ) + val demoMessage = TrackMessage.create( + "test1", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + //destination plugin list + whenever(chain.plugins).thenReturn( + listOf( + createDummyDestinationPluginWithName("Firebase"), + createDummyDestinationPluginWithName("Mixpanel"), + ) + ) + var chainCopy: Plugin.Chain? = null + whenever(chain.with(anyList())).then { + val list = it.arguments[0] as List + CentralPluginChain(demoMessage, list, originalMessage = demoMessage).also { chainCopy = it } + } + // When + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + eventFilteringPlugin.intercept(chain) + + // Then + verify(chain, times(1)).with(anyOrNull()) + MatcherAssert.assertThat( + chainCopy, allOf( + org.hamcrest.Matchers.notNullValue(), + org.hamcrest.Matchers.instanceOf(CentralPluginChain::class.java), + org.hamcrest.Matchers.hasProperty( + "plugins", + allOf( + Matchers.iterableWithSize(1), not( + Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Firebase")) + ) + ) + ), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Mixpanel")) + ) + ) + ) + ) + ) + ) + //a copy of chain is made since filteredEventsMap is not empty + verify(chain, never()).proceed(demoMessage) + } + + @Test + fun `disallow destination plugins when event name not in white list`() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource( + destinations = listOf( + RudderServerConfig.RudderServerDestination( + destinationId = "test", + destinationName = "TestDestination", + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "Firebase", + ), + isDestinationEnabled = true, + updatedAt = "2022-01-01", + destinationConfig = mapOf( + "eventFilteringOption" to "whitelistedEvents", + "whitelistedEvents" to setOf( + mapOf("eventName" to "test1"), mapOf + ( + "eventName" to + "test2", + ), mapOf("eventName" to "test3") + ) + ) + ), RudderServerConfig.RudderServerDestination( + destinationId = "test2", + destinationName = "TestDestination2", + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "Braze", + ), + isDestinationEnabled = true, + updatedAt = "2022-01-01", + destinationConfig = mapOf( + "eventFilteringOption" to "blacklistedEvents", + "blacklistedEvents" to setOf( + mapOf("eventName" to "test2"), mapOf + ( + "eventName" to + "test2", + ), mapOf("eventName" to "test3") + ) + ) + ) + ) + ) + ) + val demoMessage = TrackMessage.create( + "testEvent", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + //destination plugin list + whenever(chain.plugins).thenReturn( + listOf( + createDummyDestinationPluginWithName("Firebase"), + createDummyDestinationPluginWithName("Mixpanel"), + ) + ) + var chainCopy: Plugin.Chain? = null + whenever(chain.with(anyList())).then { + val list = it.arguments[0] as List + CentralPluginChain(demoMessage, list, originalMessage = demoMessage).also { chainCopy = it } + } + // When + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + eventFilteringPlugin.intercept(chain) + + // Then + verify(chain, times(1)).with(anyOrNull()) + MatcherAssert.assertThat( + chainCopy, allOf( + org.hamcrest.Matchers.notNullValue(), + org.hamcrest.Matchers.instanceOf(CentralPluginChain::class.java), + org.hamcrest.Matchers.hasProperty( + "plugins", + allOf( + Matchers.iterableWithSize(1), not( + Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Firebase")) + ) + ) + ), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Mixpanel")) + ) + ) + ) + ) + ) + ) + //a copy of chain is made since filteredEventsMap is not empty + verify(chain, never()).proceed(demoMessage) + } + + @Test + fun `allow destination plugins when event name in white list`() { + // Given + val chain = mock() + val serverConfig = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource( + destinations = listOf( + RudderServerConfig.RudderServerDestination( + destinationId = "test", + destinationName = "TestDestination", + destinationDefinition = RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "Firebase", + ), + isDestinationEnabled = true, + updatedAt = "2022-01-01", + destinationConfig = mapOf( + "eventFilteringOption" to "whitelistedEvents", + "whitelistedEvents" to setOf( + mapOf("eventName" to "test1"), mapOf( + "eventName" to + "test2", + ), mapOf("eventName" to "test3") + ) + ) + ) + ) + ) + ) + val demoMessage = TrackMessage.create( + "test1", anonymousId = "anon-1", timestamp = + Date().toString() + ) + whenever(chain.message()).thenReturn(demoMessage) + //destination plugin list + whenever(chain.plugins).thenReturn( + listOf( + createDummyDestinationPluginWithName("Firebase"), + createDummyDestinationPluginWithName("Mixpanel"), + ) + ) + var chainCopy: Plugin.Chain? = null + whenever(chain.with(anyList())).then { + val list = it.arguments[0] as List + CentralPluginChain(demoMessage, list, originalMessage = demoMessage).also { chainCopy = it } + } + // When + eventFilteringPlugin.updateRudderServerConfig(serverConfig) + + eventFilteringPlugin.intercept(chain) + + // Then + verify(chain, times(1)).with(anyOrNull()) + MatcherAssert.assertThat( + chainCopy, allOf( + org.hamcrest.Matchers.notNullValue(), + org.hamcrest.Matchers.instanceOf(CentralPluginChain::class.java), + org.hamcrest.Matchers.hasProperty( + "plugins", + allOf( + Matchers.iterableWithSize(2), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Firebase")) + ) + ), Matchers.hasItem( + allOf( + Matchers.instanceOf> + (BaseDestinationPlugin::class.java), hasProperty("name", equalTo("Mixpanel")) + ) + ) + ) + ) + ) + ) + //a copy of chain is made since filteredEventsMap is not empty + verify(chain, never()).proceed(demoMessage) + } + + private fun createDummyDestinationPluginWithName(name: String): DestinationPlugin<*> { + return BaseDestinationPlugin(name) { + it.proceed(it.message()) + } + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/EventSizeFilterPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/EventSizeFilterPluginTest.kt new file mode 100644 index 000000000..4f859489d --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/EventSizeFilterPluginTest.kt @@ -0,0 +1,97 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.RudderUtils.MAX_EVENT_SIZE +import com.rudderstack.core.RudderUtils.getUTF8Length +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.gsonrudderadapter.GsonAdapter +import org.junit.Test + +class EventSizeFilterPluginTest { + + private val eventSizeFilterPlugin = EventSizeFilterPlugin() + private val currentConfiguration = Configuration(jsonAdapter = GsonAdapter()) + + @Test + fun `given event size does not exceed the maximum size, then the next plugin in the chain should be called`() { + var isCalled = false + val testPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + isCalled = true + return chain.proceed(chain.message()) + } + } + val message = getMessageUnderMaxSize() + val eventSizeFilterTestChain = CentralPluginChain( + message, listOf( + eventSizeFilterPlugin, testPlugin + ), originalMessage = message + ) + eventSizeFilterPlugin.updateConfiguration(currentConfiguration) + + val returnedMsg = eventSizeFilterTestChain.proceed(message) + assert(returnedMsg == message) + assert(isCalled) + } + + @Test + fun `given event size exceeds the maximum size, then the next plugin in the chain should not be called`() { + var isCalled = false + val testPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + isCalled = true + return chain.proceed(chain.message()) + } + } + val message = getMessageOverMaxSize() + val eventSizeFilterTestChain = CentralPluginChain( + message, + listOf(eventSizeFilterPlugin, testPlugin), + originalMessage = message + ) + eventSizeFilterPlugin.updateConfiguration(currentConfiguration) + + val returnedMsg = eventSizeFilterTestChain.proceed(message) + assert(returnedMsg == message) + assert(!isCalled) + } + + private fun getMessageUnderMaxSize(): Message { + return TrackMessage.create( + "ev-1", RudderUtils.timeStamp, traits = mapOf( + "age" to 31, "office" to "Rudderstack" + ), externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), customContextMap = null + ).also { message -> + val messageJSON = currentConfiguration.jsonAdapter.writeToJson(message) + val messageSize = messageJSON.toString().getUTF8Length() + assert(messageSize < MAX_EVENT_SIZE) + } + } + + private fun getMessageOverMaxSize(): Message { + fun generateDataOfSize(msgSize: Int): String { + return CharArray(msgSize).apply { fill('a') }.joinToString("") + } + + val properties = mutableMapOf() + properties["property"] = generateDataOfSize(1024 * 33) + + return TrackMessage.create( + "ev-1", RudderUtils.timeStamp, properties = properties + ).also { message -> + val messageJSON = currentConfiguration.jsonAdapter.writeToJson(message) + val messageSize = messageJSON.toString().getUTF8Length() + assert(messageSize > MAX_EVENT_SIZE) + } + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/GDPRPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/GDPRPluginTest.kt new file mode 100644 index 000000000..db5ff20b1 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/GDPRPluginTest.kt @@ -0,0 +1,76 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import io.mockk.every +import io.mockk.mockk +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.Test + +class GDPRPluginTest { + private val gdprPlugin = GDPRPlugin() + private val message = TrackMessage.create( + "ev-1", RudderUtils.timeStamp, + traits = mapOf( + "age" to 31, + "office" to "Rudderstack" + ), + externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), + customContextMap = null + ) + + @Test + fun `test gdpr with opt out`() { + val analytics = mockk(relaxed = true) + every { analytics.storage.isOptedOut } returns true + val testPluginForOptOut = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assert(false) + return chain.proceed(chain.message()) + } + } + val optOutTestChain = CentralPluginChain( + message, listOf(gdprPlugin, testPluginForOptOut), originalMessage = message + ) + //opted out + gdprPlugin.setup(analytics) + //check for opt out + val returnedMsg = optOutTestChain.proceed(message) + assertThat(returnedMsg, Matchers.`is`(returnedMsg)) + } + + @Test + fun `test gdpr with opt in`() { + val analytics = mockk(relaxed = true) + every { analytics.storage.isOptedOut } returns false + + var isCalled = false + val testPluginForOptIn = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + isCalled = true + return chain.proceed(chain.message()) + } + } + + val optInTestChain = CentralPluginChain( + message, listOf(gdprPlugin, testPluginForOptIn), + originalMessage = message + ) + //opted out + gdprPlugin.setup(analytics) + //check for opt out + val returnedMsg = optInTestChain.proceed(message) + assertThat(returnedMsg, Matchers.`is`(returnedMsg)) + assertThat(isCalled, Matchers.`is`(true)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/RudderOptionPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/RudderOptionPluginTest.kt new file mode 100644 index 000000000..fb9ae6130 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/RudderOptionPluginTest.kt @@ -0,0 +1,140 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.BaseDestinationPlugin +import com.rudderstack.core.Plugin +import com.rudderstack.core.RudderOption +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.TrackMessage +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.everyItem +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.hasItems +import org.hamcrest.Matchers.`in` +import org.hamcrest.Matchers.iterableWithSize +import org.hamcrest.Matchers.not +import org.junit.Test + +class RudderOptionPluginTest { + + //for test we create 3 destinations + private val dest1 = BaseDestinationPlugin("dest-1") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + + private val dest2 = BaseDestinationPlugin("dest-2") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + private val dest3 = BaseDestinationPlugin("dest-3") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + private val message = TrackMessage.create( + "ev-1", RudderUtils.timeStamp, + traits = mapOf( + "age" to 31, + "office" to "Rudderstack" + ), + externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), + customContextMap = null + ) + + @Test + fun `test all true for empty integrations`() { + //assertion plugin + val assertPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat(chain.plugins, allOf(iterableWithSize(5), hasItems(dest1, dest2, dest3))) + return chain.proceed(chain.message()) + } + } + + val chain = CentralPluginChain( + message, listOf( + RudderOptionPlugin(RudderOption()), assertPlugin, dest1, dest2, dest3 + ), originalMessage = message + ) + chain.proceed(message) + } + + @Test + fun `test all false for integrations`() { + //assertion plugin + val assertPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat(chain.plugins, allOf(iterableWithSize(2), everyItem(not(`in`(arrayOf(dest1, dest2, dest3)))))) + return chain.proceed(chain.message()) + } + } + val chain = CentralPluginChain( + message, listOf( + RudderOptionPlugin( + RudderOption() + .putIntegration("All", false) + ), assertPlugin, dest1, dest2, dest3 + ), originalMessage = message + ) + chain.proceed(message) + } + + @Test + fun `test custom integrations with false`() { + //assertion plugin + val assertPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat( + chain.plugins, allOf( + iterableWithSize(3), + everyItem(not(`in`(arrayOf(dest2, dest3)))), + hasItem(dest1) + ) + ) + return chain.proceed(chain.message()) + } + } + val chain = CentralPluginChain( + message, listOf( + RudderOptionPlugin( + RudderOption() + .putIntegration("dest-2", false) + .putIntegration("dest-3", false) + ), assertPlugin, dest1, dest2, dest3 + ), originalMessage = message + ) + chain.proceed(message) + } + + @Test + fun `test custom integrations with true`() { + //assertion plugin + val assertPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + assertThat( + chain.plugins, allOf(iterableWithSize(3), everyItem(not(`in`(arrayOf(dest1, dest3)))), hasItem(dest2)) + ) + return chain.proceed(chain.message()) + } + } + val chain = CentralPluginChain( + message, listOf( + RudderOptionPlugin( + RudderOption() + .putIntegration("All", false) + .putIntegration("dest-2", true) + .putIntegration("dest-3", false) + ), assertPlugin, dest1, dest2, dest3 + ), originalMessage = message + ) + chain.proceed(message) + } + +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/StoragePluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/StoragePluginTest.kt new file mode 100644 index 000000000..b8282c441 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/StoragePluginTest.kt @@ -0,0 +1,100 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.Storage +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.models.TrackMessage +import com.vagabond.testcommon.VerificationStorage +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.iterableWithSize +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock + +class StoragePluginTest { + + private val testMessagesList = listOf( + TrackMessage.create("m-1", anonymousId = "anon-1", timestamp = "09-01-2022"), + TrackMessage.create("m-2", anonymousId = "anon-2", timestamp = "09-01-2022"), + TrackMessage.create("m-3", anonymousId = "anon-3", timestamp = "09-01-2022"), + TrackMessage.create("m-4", anonymousId = "anon-4", timestamp = "09-01-2022"), + TrackMessage.create("m-5", anonymousId = "anon-5", timestamp = "09-01-2022"), + TrackMessage.create("m-6", anonymousId = "anon-6", timestamp = "09-01-2022"), + TrackMessage.create("m-7", anonymousId = "anon-7", timestamp = "09-01-2022"), + TrackMessage.create("m-8", anonymousId = "anon-8", timestamp = "09-01-2022"), + TrackMessage.create("m-9", anonymousId = "anon-9", timestamp = "09-01-2022"), + TrackMessage.create("m-10", anonymousId = "anon-10", timestamp = "09-01-2022"), + TrackMessage.create("m-11", anonymousId = "anon-11", timestamp = "09-01-2022"), + ) + private lateinit var analytics: Analytics + private lateinit var storage: Storage + @Before + fun setup() { + storage = VerificationStorage() + analytics = generateTestAnalytics( + mockConfiguration = Configuration(mock(), shouldVerifySdk = false), storage = storage + ) + } + @After + fun tearDown() { + analytics.shutdown() + } + + @Test + fun testStoragePluginWithQueueSize() { + + val eventNames = testMessagesList.map { + it.eventName + } + val storagePlugin = StoragePlugin() + storagePlugin.setup(analytics) +// storagePlugin.updateConfiguration() + testMessagesList.forEach { msg -> + CentralPluginChain(msg, listOf(storagePlugin), originalMessage = msg).proceed(msg) + } + val dataOfNames = storage.getDataSync().map { + (it as TrackMessage).eventName + } + assertThat( + dataOfNames, allOf( + iterableWithSize(testMessagesList.size), contains(*eventNames.toTypedArray()) + ) + ) + } + + + /*@Test + fun testStoragePluginWithFlushInterval() { + val isComplete = AtomicBoolean(false) + val flushInterval = 3 * 1000L + val configuration = Configuration( + jsonAdapter = JacksonAdapter(), + flushQueueSize = Integer.MAX_VALUE, // setting it to a large value ensures + // there is no chance of flushing due to queue size + maxFlushInterval = flushInterval + ) + ConfigurationsState.update(configuration) + val storagePluginCreated = System.currentTimeMillis() + val storagePlugin = StoragePlugin(*//*FlushScheduler(BasicStorageImpl(logger = KotlinLogger)) { + + assertThat( + (System.currentTimeMillis() - storagePluginCreated), allOf( + greaterThanOrEqualTo(flushInterval)*//**//*, + lessThan(2 * flushInterval )*//**//* + ) + ) + println("done") + isComplete.set(true) + }*//*) + testMessagesList.forEach { msg -> + CentralPluginChain(msg, listOf(storagePlugin)).proceed(msg) + } + Awaitility.await().atMost(1, TimeUnit.MINUTES).untilTrue(isComplete) + + }*/ +} diff --git a/core/src/test/java/com/rudderstack/core/internal/plugins/WakeupActionPluginTest.kt b/core/src/test/java/com/rudderstack/core/internal/plugins/WakeupActionPluginTest.kt new file mode 100644 index 000000000..d41c21444 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/internal/plugins/WakeupActionPluginTest.kt @@ -0,0 +1,121 @@ +package com.rudderstack.core.internal.plugins + +import com.rudderstack.core.Analytics +import com.rudderstack.core.BaseDestinationPlugin +import com.rudderstack.core.BasicStorageImpl +import com.rudderstack.core.Configuration +import com.rudderstack.core.DestinationConfig +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.holder.retrieveState +import com.rudderstack.core.internal.CentralPluginChain +import com.rudderstack.core.internal.states.DestinationConfigState +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.core.models.TrackMessage +import com.vagabond.testcommon.generateTestAnalytics +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * Wake up action plugin forwards only those destination plugins, that have initialized. + * In case each destination plugin is not initialized, it stores the message in startup queue. + * + * The testing purpose would be to check if startup queue is storing the messages for late + * initialized destinations + */ +class WakeupActionPluginTest { + private val dest1 = BaseDestinationPlugin("dest-1") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + + private val dest2 = BaseDestinationPlugin("dest-2") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + private val dest3 = BaseDestinationPlugin("dest-3") { + return@BaseDestinationPlugin it.proceed(it.message()) + } + private val storage = BasicStorageImpl() + private val wakeupActionPlugin = WakeupActionPlugin() + private val testMessage = TrackMessage.create( + "ev-1", RudderUtils.timeStamp, traits = mapOf( + "age" to 31, "office" to "Rudderstack" + ), externalIds = listOf( + mapOf("some_id" to "s_id"), + mapOf("amp_id" to "amp_id"), + ), customContextMap = null + ) + + private lateinit var analytics: Analytics + + @Before + fun setup() { + analytics = generateTestAnalytics( + Configuration(jsonAdapter = JacksonAdapter(), + shouldVerifySdk = false), storage = storage + ) + wakeupActionPlugin.setup(analytics) + } + private val destinationConfigState + get() = analytics.retrieveState() + + @After + fun breakDown() { + analytics.shutdown() + dest1.setReady(false) + dest2.setReady(false) + dest3.setReady(false) + + storage.clearStartupQueue() + //clear destination config + destinationConfigState?.update(DestinationConfig()) + } + + @Test + fun `check startup queue for uninitialized destinations`() { + dest1.setReady(false) + dest2.setReady(true) + dest3.setReady(true) + destinationConfigState?.update( + DestinationConfig( + mapOf( + "dest-1" to dest1.isReady, + "dest-2" to dest2.isReady, + "dest-3" to dest3.isReady, + ) + ) + ) + val plugins = listOf(wakeupActionPlugin) + val centralPluginChain = CentralPluginChain(testMessage, plugins, originalMessage = testMessage) + centralPluginChain.proceed(testMessage) + //dest1 is not ready, hence message should be stored + assertThat( + storage.startupQueue, allOf( + iterableWithSize(1), hasItem(testMessage) + ) + ) + } + + @Test + fun `check startup queue for initialized destinations`() { + dest1.setReady(true) + dest2.setReady(true) + dest3.setReady(true) + + destinationConfigState?.update( + DestinationConfig( + mapOf( + "dest-1" to dest1.isReady, + "dest-2" to dest2.isReady, + "dest-3" to dest3.isReady, + ) + ) + ) + val plugins = listOf(wakeupActionPlugin) + val centralPluginChain = CentralPluginChain(testMessage, plugins, originalMessage = testMessage) + centralPluginChain.proceed(testMessage) + //dest1 is not ready, hence message should be stored + assertThat(storage.startupQueue, iterableWithSize(0)) + } +} diff --git a/core/src/test/java/com/rudderstack/core/models/JsonUtilities.kt b/core/src/test/java/com/rudderstack/core/models/JsonUtilities.kt new file mode 100644 index 000000000..318afad12 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/models/JsonUtilities.kt @@ -0,0 +1,26 @@ +package com.rudderstack.core.models + +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import java.io.BufferedReader +import java.io.IOException + +object MockResponse { + + var jsonAdapter: JsonAdapter = JacksonAdapter() + + @Throws(NullPointerException::class, IOException::class) + inline fun fromJsonFile(jsonPath: String, typeAdapter: RudderTypeAdapter): T? { + val reader = javaClass.classLoader?.getResourceAsStream(jsonPath)?.reader() + ?.let { BufferedReader(it) } + ?: throw IOException("Null InputStream.") + val content: String + reader.use { content = it.readText() } + return try { + jsonAdapter.readJson(content, typeAdapter) + } catch (exception: Exception) { + throw IOException(exception) + } + } +} diff --git a/core/src/test/java/com/rudderstack/core/models/MessageParseTest.kt b/core/src/test/java/com/rudderstack/core/models/MessageParseTest.kt new file mode 100644 index 000000000..8a11cdc60 --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/models/MessageParseTest.kt @@ -0,0 +1,297 @@ +package com.rudderstack.core.models + +import com.rudderstack.gsonrudderadapter.GsonAdapter +import com.rudderstack.jacksonrudderadapter.JacksonAdapter +import com.rudderstack.moshirudderadapter.MoshiAdapter +import com.rudderstack.rudderjsonadapter.JsonAdapter +import org.hamcrest.MatcherAssert +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.json.JSONObject +import org.junit.Test +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode + +abstract class MessageParseTest { + abstract var jsonAdapter: JsonAdapter + + companion object { + private const val TRACK_JSON = + "{\n" + + " \"type\": \"track\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \n" + + " },\n" + + " \"event\": \"Java Test\",\n" + + " \"properties\": {\n" + + " \"count\": \"1\"\n" + + " }\n" + + "}" + private const val ALIAS_JSON = "{\n" + + " \"type\": \"alias\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \n" + + " },\n" + + " \"previousId\": \"172d84b9-a684-4249-8646-0994173555cd\"\n" + + "}" + + private const val GROUP_JSON = "{\n" + + " \"type\": \"group\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \n" + + " },\n" + + " \"groupId\": \"193d84b9-a684-4249-8646-0994173555cd\",\n" + + " \"traits\": {\n" + + " \"group\": \"some_name\",\n" + + " \"journey\": \"Australia\"\n" + + " }\n" + + "}" + private const val SCREEN_JSON = "{\n" + + " \"type\": \"screen\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \n" + + " },\n" + + " \"properties\": {\n" + + " \"category\": \"login\",\n" + + " \"name\": \"first_screen\",\n" + + " \"count\": \"1\"\n" + + " }\n" + + "}" + + private const val PAGE_JSON = "{\n" + + " \"type\": \"page\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \n" + + " },\n" + + " \"event\": \"Java Test\",\n" + + " \"properties\": {\n" + + " \"count\": \"1\"\n" + + " },\n" + + " \"category\": \"some_category\"\n" + + "}" + private const val IDENTIFY_JSON = "{\n" + + " \"type\": \"identify\",\n" + + " \"messageId\": \"172d84b9-a684-4249-8646-0994173555cc\",\n" + + " \"originalTimestamp\": \"2021-11-20T15:37:19.753Z\",\n" + + " \"anonymousId\": \"bc73bb87-8fb4-4498-97c8-570299a4686d\",\n" + + " \"userId\": \"debanjanchatterjee\",\n" + + " \"context\": null,\n" + + + " \"integrations\": {\n" + + " \"firebase\": true,\n" + + " \"amplitude\": false\n" + + " },\n" + + "\"properties\": {}\n" + + "}" + } + + @Test + fun testTrackParsing() { + val track = jsonAdapter.readJson(TRACK_JSON, TrackMessage::class.java) + println("track: $track, \nchannel : ${track?.channel}") + + MatcherAssert.assertThat( + track, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.TRACK)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + hasProperty("properties", allOf(aMapWithSize(1))), + hasProperty("eventName", `is`("Java Test")), + ), + ) + assertThat(track!!.properties!!["count"], `is`("1")) + // serialization + val trackJson = jsonAdapter.writeToJson(track) + JSONAssert.assertEquals( + trackJson, + JSONObject(TRACK_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + + track.channel = "web" + JSONAssert.assertEquals( + jsonAdapter.writeToJson(track), + JSONObject(TRACK_JSON).also { + it.put("channel", "web") + }, + JSONCompareMode.LENIENT, + ) + } + + @Test + fun testAliasParsing() { + val alias = jsonAdapter.readJson(ALIAS_JSON, AliasMessage::class.java) + assertThat(alias, notNullValue()) + MatcherAssert.assertThat( + alias, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.ALIAS)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + ), + ) + val aliasJson = jsonAdapter.writeToJson(alias!!) + + JSONAssert.assertEquals( + aliasJson, + JSONObject(ALIAS_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + } + + @Test + fun testGroupParsing() { + val group = jsonAdapter.readJson(GROUP_JSON, GroupMessage::class.java) + assertThat( + group, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.GROUP)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + hasProperty("traits", allOf(aMapWithSize(2))), +// hasProperty("eventName", `is`("Java Test")) + ), + ) + assertThat(group!!.traits!!["group"], `is`("some_name")) + assertThat(group.traits!!["journey"], `is`("Australia")) + val groupJson = jsonAdapter.writeToJson(group) + + JSONAssert.assertEquals( + groupJson, + JSONObject(GROUP_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + } + + @Test + fun testScreenParsing() { + val screen = jsonAdapter.readJson(SCREEN_JSON, ScreenMessage::class.java) + assertThat( + screen, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.SCREEN)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + hasProperty("properties", allOf(aMapWithSize(3), hasEntry + ("category", "login"), hasEntry("name", "first_screen")), + ), + hasProperty("userId", `is`("debanjanchatterjee")), + ), + ) + assertThat(screen!!.properties!!["count"], `is`("1")) + val screenJson = jsonAdapter.writeToJson(screen) + + JSONAssert.assertEquals( + screenJson, + JSONObject(SCREEN_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + } + + @Test + fun testPageParsing() { + val page = jsonAdapter.readJson(PAGE_JSON, PageMessage::class.java) + assertThat( + page, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.PAGE)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + hasProperty("properties", allOf(aMapWithSize(1))), + hasProperty("userId", `is`("debanjanchatterjee")), + hasProperty("category", `is`("some_category")), + ), + ) + assertThat(page!!.properties!!["count"], `is`("1")) + val pageJson = jsonAdapter.writeToJson(page) + JSONAssert.assertEquals( + pageJson, + JSONObject(PAGE_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + } + + @Test + fun testIdentifyParsing() { + val identify = jsonAdapter.readJson(IDENTIFY_JSON, IdentifyMessage::class.java) + assertThat( + identify, + allOf( + notNullValue(), + hasProperty("type", `is`(Message.EventType.IDENTIFY)), + hasProperty("channel", `is`("server")), + hasProperty("timestamp", `is`("2021-11-20T15:37:19.753Z")), + hasProperty("integrations", allOf(aMapWithSize(2))), + ), + ) + assertThat(identify!!.integrations!!["firebase"], `is`(true)) + assertThat(identify.integrations!!["amplitude"], `is`(false)) + + val identifyJson = jsonAdapter.writeToJson(identify) + JSONAssert.assertEquals( + identifyJson, + JSONObject(IDENTIFY_JSON).also { + it.put("channel", "server") + }, + JSONCompareMode.LENIENT, + ) + } +} + +class MessageParseGsonTest : MessageParseTest() { + override var jsonAdapter: JsonAdapter = GsonAdapter() +} + +class MessageParseJacksonTest : MessageParseTest() { + override var jsonAdapter: JsonAdapter = JacksonAdapter() +} + +class MessageParseMoshiTest : MessageParseTest() { + override var jsonAdapter: JsonAdapter = MoshiAdapter() +} diff --git a/core/src/test/java/com/rudderstack/core/models/MessageUtilsTest.kt b/core/src/test/java/com/rudderstack/core/models/MessageUtilsTest.kt new file mode 100644 index 000000000..55c259cbf --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/models/MessageUtilsTest.kt @@ -0,0 +1,126 @@ +package com.rudderstack.core.models + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasEntry +import org.hamcrest.Matchers.hasItems +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class MessageUtilsTest { + @Test + fun `given both the contexts are null, when optAddContext is called, then it should return null`() { + val context1: MessageContext? = null + val context2: MessageContext? = null + + val result = context1 optAddContext context2 + + assertNull(result) + } + + @Test + fun `given first context is null, when optAddContext is called, then it should return second context`() { + val context1: MessageContext? = null + val context2: MessageContext = mapOf("key" to "value") + + val result = context1 optAddContext context2 + + assertEquals(context2, result) + } + + @Test + fun `given second context is null, when optAddContext is called, then it should return first context`() { + val context1: MessageContext = mapOf("key" to "value") + val context2: MessageContext? = null + + val result = context1 optAdd context2 + + assertEquals(context1, result) + } + + @Test + fun `given there are overlapping keys in both contexts, when optAddContext is called, then it should return the merged context while prioritizing the key-value pair from the first context`() { + val context1: MessageContext = createContext( + mapOf("trait1" to "value1", "common" to "value1") + ) + val context2: MessageContext = createContext( + mapOf("trait2" to "value2", "common" to "value2") + ) + + val result = context1 optAddContext context2 + + assertThat( + result?.traits, allOf( + hasEntry("trait1", "value1"), + hasEntry("trait2", "value2"), + hasEntry("common", "value1"), + ) + ) + } + + @Test + fun `given there are no overlapping keys in both contexts, when optAddContext is called, then it should return the merged context`() { + val context1: MessageContext = mapOf( + Constants.CUSTOM_CONTEXT_MAP_ID to mapOf("context1" to "value1") + ) + val context2: MessageContext = mapOf( + Constants.CUSTOM_CONTEXT_MAP_ID to mapOf("context2" to "value2") + ) + + val result = context1 optAddContext context2 + + assertThat( + result?.customContexts, allOf( + hasEntry("context1", "value1"), + hasEntry("context2", "value2"), + ) + ) + } + + @Test + fun `given there are overlapping externalIds in both contexts, when optAddContext is called, then it should return the merged context while prioritizing the key-value pair from the first context`() { + val currentEventContext: MessageContext = createContext( + externalIds = listOf( + mapOf("type" to "brazeExternalID", "id" to "braze-67890-override"), + mapOf("type" to "amplitudeExternalID", "id" to "amp-5678-override"), + mapOf("type" to "firebaseExternalID", "id" to "fire-67890"), + ) + ) + val savedContext: MessageContext = createContext( + externalIds = listOf( + mapOf("type" to "brazeExternalID", "id" to "braze-1234"), + mapOf("type" to "amplitudeExternalID", "id" to "amp-5678"), + mapOf("type" to "adobeExternalID", "id" to "fire-67890"), + ) + ) + + val result = currentEventContext optAddContext savedContext + + assertThat( + result?.externalIds, + hasItems( + mapOf("type" to "brazeExternalID", "id" to "braze-67890-override"), + mapOf("type" to "amplitudeExternalID", "id" to "amp-5678-override"), + mapOf("type" to "adobeExternalID", "id" to "fire-67890"), + mapOf("type" to "firebaseExternalID", "id" to "fire-67890"), + ) + ) + } + + @Test + fun `given there are overlapping extra keys in both contexts, when optAddContext is called, then it should return the merged context while prioritizing the key-value pair from the first context`() { + val context1: MessageContext = mapOf("extra1" to "value1", "common" to "value1") + val context2: MessageContext = mapOf("extra2" to "value2", "common" to "value2") + + val result = context1 optAddContext context2 + + assertThat( + result, allOf( + hasEntry("extra1", "value1"), + hasEntry("extra2", "value2"), + hasEntry("common", "value1"), + ) + ) + } +} diff --git a/core/src/test/java/com/rudderstack/core/models/RudderServerConfigParseTest.kt b/core/src/test/java/com/rudderstack/core/models/RudderServerConfigParseTest.kt new file mode 100644 index 000000000..e09c9e34b --- /dev/null +++ b/core/src/test/java/com/rudderstack/core/models/RudderServerConfigParseTest.kt @@ -0,0 +1,170 @@ +package com.rudderstack.core.models + +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + + +private const val rudderServerConfigMockResponse1 = "mock/rudder_config_response.json" +private const val rudderServerConfigMockResponse2 = "mock/rudder_config_response_2.json" + +open class RudderServerConfigParseTest { + + @Test + fun `given a rudder server config response that includes destinations, when the response is parsed, then assert that all destinations have an api key`() { + // deserialize + val rta = object : RudderTypeAdapter() {} + val res = MockResponse.fromJsonFile(rudderServerConfigMockResponse1, rta) + assert(res != null) + assertThat( + res?.source?.destinations?.get(1), + allOf( + notNullValue(), + isA(RudderServerConfig.RudderServerDestination::class.java), + ), + ) + assertThat( + res?.source?.destinations?.get(1)?.destinationConfig?.get("apiKey"), + allOf( + notNullValue(), + `is`("1234abcd"), + ), + ) + } + + @Test + fun `given a rudder server config response that includes transformations, when the response is parsed, then assert that all destinations have transformations enabled`() { + // deserialize + val rta = object : RudderTypeAdapter() {} + val res = MockResponse.fromJsonFile(rudderServerConfigMockResponse2, rta) + assert(res != null) + assertThat( + res?.source?.destinations?.get(0), + allOf( + notNullValue(), + isA(RudderServerConfig.RudderServerDestination::class.java), + ), + ) + assertThat( + res?.source?.destinations?.get(0)?.destinationId, + allOf( + notNullValue(), + `is`("20NBa9wa4Zb5ZHkJHO2IEiw8eWl"), + ), + ) + assertThat( + res?.source?.destinations?.get(0)?.areTransformationsConnected, + allOf( + notNullValue(), + `is`(true), + ), + ) + } + + @Test + fun `given a rudderServerConfig object, when serializing, then the operation completes successfully`() { + val objectToSave = provideRudderServerConfig() + + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos).also { it.writeObject(objectToSave) } + val savedBytes = baos.toByteArray() + oos.close() + + val bais = ByteArrayInputStream(savedBytes) + val ois = ObjectInputStream(bais) + val savedObject = ois.readObject() + ois.close() + + assertThat( + savedObject, allOf( + isA(RudderServerConfig::class.java), + hasProperty("hosted", `is`(true)), + hasProperty( + "source", allOf( + isA(RudderServerConfig.RudderServerConfigSource::class.java), + hasProperty("sourceId", `is`("sourceId")), + hasProperty("sourceName", `is`("sourceName")), + hasProperty("sourceEnabled", `is`(true)), + hasProperty("updatedAt", `is`("2020-02-26T09:17:52.231Z")), + hasProperty( + "destinations", allOf>( + iterableWithSize(1), + hasItem( + allOf( + hasProperty("destinationId", `is`("d_id")), + hasProperty("destinationName", `is`("d_name")), + hasProperty("destinationEnabled", `is`(true)), + hasProperty("areTransformationsConnected", `is`(true)), + hasProperty("updatedAt", `is`("2021-02-26T09:17:52.231Z")), + hasProperty( + "destinationConfig", allOf>( + aMapWithSize(1), + hasEntry("config", "config_v") + ) + ), + hasProperty( + "destinationDefinition", + allOf( + isA(RudderServerConfig.RudderServerDestinationDefinition::class.java), + hasProperty("definitionName", `is`("d_d_name")), + hasProperty("displayName", `is`("d_disp_name")), + hasProperty( + "updatedAt", + `is`("2022-02-26T09:17:52.231Z") + ), + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + } +} + +private fun provideRudderServerConfig() = RudderServerConfig( + isHosted = true, + source = RudderServerConfig.RudderServerConfigSource( + sourceId = "sourceId", + sourceName = "sourceName", + isSourceEnabled = true, + updatedAt = "2020-02-26T09:17:52.231Z", + destinations = provideServerConfigDestinationList() + ) +) + +private fun provideServerConfigDestinationList(): List { + return listOf( + provideServerConfigDestination() + ) +} + +private fun provideServerConfigDestination(): RudderServerConfig.RudderServerDestination { + return RudderServerConfig.RudderServerDestination( + destinationId = "d_id", + destinationName = "d_name", + isDestinationEnabled = true, + updatedAt = "2021-02-26T09:17:52.231Z", + destinationDefinition = provideDestinationDefinition(), + destinationConfig = mapOf("config" to "config_v"), + areTransformationsConnected = true + ) +} + +private fun provideDestinationDefinition(): RudderServerConfig.RudderServerDestinationDefinition { + return RudderServerConfig.RudderServerDestinationDefinition( + definitionName = "d_d_name", + displayName = "d_disp_name", + updatedAt = "2022-02-26T09:17:52.231Z" + ) +} + + diff --git a/core/src/test/resources/mock/rudder_config_response.json b/core/src/test/resources/mock/rudder_config_response.json new file mode 100644 index 000000000..04feecbc2 --- /dev/null +++ b/core/src/test/resources/mock/rudder_config_response.json @@ -0,0 +1,279 @@ +{ + "isHosted":true, + "source":{ + "config":{ + + }, + "id":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "name":"TestAndroid", + "writeKey":"1xXCubSHWXbpBI2h6EpCjKOsxmQ", + "enabled":true, + "sourceDefinitionId":"1QGzOQGVLM35GgtteFH1vYCE0WT", + "createdBy":"1xXC9Q9pYMrw0OON9Aw2AFeBsUp", + "workspaceId":"1xXCSVsmPjqFKhRlpajnDXkGwYX", + "deleted":false, + "createdAt":"2021-09-01T10:31:09.414Z", + "updatedAt":"2021-09-01T10:31:09.414Z", + "connections":[ + { + "id":"20NBaKc9N7u13PPOVb3CNfozNvo", + "sourceId":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "destinationId":"20NBa9wa4Zb5ZHkJHO2IEiw8eWl", + "enabled":true, + "deleted":false, + "createdAt":"2021-11-02T17:47:06.381Z", + "updatedAt":"2021-11-02T17:47:06.381Z" + }, + { + "id":"20Pa0p59biSDa8y0cgjLh7JTP05", + "sourceId":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "destinationId":"20Pa0T3VoQZZYPgh5LQS2vuwg3N", + "enabled":true, + "deleted":false, + "createdAt":"2021-11-03T14:07:35.341Z", + "updatedAt":"2021-11-03T14:07:35.341Z" + } + ], + "destinations":[ + { + "config":{ + + }, + "secretConfig":{ + + }, + "id":"20NBa9wa4Zb5ZHkJHO2IEiw8eWl", + "name":"androidTest-firebase", + "enabled":true, + "workspaceId":"1xXCSVsmPjqFKhRlpajnDXkGwYX", + "deleted":false, + "createdAt":"2021-11-02T17:47:05.153Z", + "updatedAt":"2021-11-02T17:47:05.153Z", + "destinationDefinition":{ + "config":{ + "destConfig":{ + "ios":[ + "useNativeSDK" + ], + "unity":[ + "useNativeSDK" + ], + "android":[ + "useNativeSDK" + ], + "reactnative":[ + "useNativeSDK" + ], + "defaultConfig":[ + + ] + }, + "secretKeys":[ + + ], + "excludeKeys":[ + + ], + "includeKeys":[ + + ], + "transformAt":"processor", + "transformAtV1":"processor", + "supportedSourceTypes":[ + "android", + "ios", + "unity", + "reactnative", + "flutter" + ], + "saveDestinationResponse":false + }, + "configSchema":null, + "responseRules":null, + "id":"1YL4j4RpSLloVaMwKrOoXLfiryj", + "name":"FIREBASE", + "displayName":"Firebase", + "category":null, + "createdAt":"2020-02-26T09:17:52.231Z", + "updatedAt":"2021-11-01T17:29:27.375Z" + } + }, + { + "config":{ + "apiKey":"1234abcd", + "groupTypeTrait":"test", + "groupValueTrait":"test_name", + "trackAllPages":false, + "trackCategorizedPages":true, + "trackNamedPages":true, + "traitsToIncrement":[ + { + "traits":"" + } + ], + "traitsToSetOnce":[ + { + "traits":"" + } + ], + "traitsToAppend":[ + { + "traits":"" + } + ], + "traitsToPrepend":[ + { + "traits":"" + } + ], + "trackProductsOnce":true, + "trackRevenuePerProduct":false, + "eventUploadPeriodMillis":30000, + "eventUploadThreshold":30, + "versionName":"", + "enableLocationListening":false, + "useAdvertisingIdForDeviceId":false, + "trackSessionEvents":true + }, + "secretConfig":{ + + }, + "id":"20Pa0T3VoQZZYPgh5LQS2vuwg3N", + "name":"android-test-amplitude", + "enabled":true, + "workspaceId":"1xXCSVsmPjqFKhRlpajnDXkGwYX", + "deleted":false, + "createdAt":"2021-11-03T14:07:33.995Z", + "updatedAt":"2021-11-03T14:07:33.995Z", + "destinationDefinition":{ + "config":{ + "destConfig":{ + "ios":[ + "eventUploadPeriodMillis", + "eventUploadThreshold", + "useNativeSDK", + "trackSessionEvents", + "useIdfaAsDeviceId" + ], + "web":[ + "useNativeSDK", + "preferAnonymousIdForDeviceId", + "deviceIdFromUrlParam", + "forceHttps", + "trackGclid", + "trackReferrer", + "saveParamsReferrerOncePerSession", + "trackUtmProperties", + "unsetParamsReferrerOnNewSession", + "batchEvents", + "eventUploadPeriodMillis", + "eventUploadThreshold", + "blackListedEvents" + ], + "android":[ + "eventUploadPeriodMillis", + "eventUploadThreshold", + "useNativeSDK", + "enableLocationListening", + "trackSessionEvents", + "useAdvertisingIdForDeviceId" + ], + "defaultConfig":[ + "apiKey", + "groupTypeTrait", + "groupValueTrait", + "trackAllPages", + "trackCategorizedPages", + "trackNamedPages", + "traitsToIncrement", + "traitsToSetOnce", + "traitsToAppend", + "traitsToPrepend", + "trackProductsOnce", + "trackRevenuePerProduct", + "versionName" + ] + }, + "secretKeys":[ + "apiKey" + ], + "excludeKeys":[ + + ], + "includeKeys":[ + "apiKey", + "groupTypeTrait", + "groupValueTrait", + "trackAllPages", + "trackCategorizedPages", + "trackNamedPages", + "traitsToIncrement", + "traitsToSetOnce", + "traitsToAppend", + "traitsToPrepend", + "trackProductsOnce", + "trackRevenuePerProduct", + "preferAnonymousIdForDeviceId", + "deviceIdFromUrlParam", + "forceHttps", + "trackGclid", + "trackReferrer", + "saveParamsReferrerOncePerSession", + "trackUtmProperties", + "unsetParamsReferrerOnNewSession", + "batchEvents", + "eventUploadPeriodMillis", + "eventUploadThreshold", + "versionName", + "enableLocationListening", + "useAdvertisingIdForDeviceId", + "trackSessionEvents", + "useIdfaAsDeviceId", + "blackListedEvents" + ], + "transformAt":"processor", + "transformAtV1":"processor", + "supportedSourceTypes":[ + "android", + "ios", + "web", + "unity", + "amp", + "cloud", + "warehouse", + "reactnative", + "flutter", + "cordova" + ], + "supportedMessageTypes":[ + "alias", + "group", + "identify", + "page", + "screen", + "track" + ], + "saveDestinationResponse":true + }, + "configSchema":null, + "responseRules":null, + "id":"1QGzO4fWSyq3lsyFHf4eQAMDSr9", + "name":"AM", + "displayName":"Amplitude", + "category":null, + "createdAt":"2019-09-02T08:08:05.613Z", + "updatedAt":"2021-11-01T17:27:45.061Z" + } + } + ], + "sourceDefinition":{ + "id":"1QGzOQGVLM35GgtteFH1vYCE0WT", + "name":"Android", + "options":null, + "displayName":"Android", + "category":null, + "createdAt":"2019-09-02T08:08:08.373Z", + "updatedAt":"2020-06-18T11:54:00.449Z" + } + } +} diff --git a/core/src/test/resources/mock/rudder_config_response_2.json b/core/src/test/resources/mock/rudder_config_response_2.json new file mode 100644 index 000000000..495aea1c5 --- /dev/null +++ b/core/src/test/resources/mock/rudder_config_response_2.json @@ -0,0 +1,136 @@ +{ + "isHosted":true, + "source":{ + "config":{ + "statsCollection":{ + "errorReports":{ + "enabled":false + }, + "metrics":{ + "enabled":false + } + } + }, + "liveEventsConfig":{ + "eventUpload":false, + "eventUploadTS":1659967927971 + }, + "id":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "name":"TestAndroid", + "writeKey":"1xXCubSHWXbpBI2h6EpCjKOsxmQ", + "enabled":true, + "sourceDefinitionId":"1QGzOQGVLM35GgtteFH1vYCE0WT", + "createdBy":"1xXC9Q9pYMrw0OON9Aw2AFeBsUp", + "workspaceId":"1xXCSVsmPjqFKhRlpajnDXkGwYX", + "deleted":false, + "transient":false, + "secretVersion":null, + "createdAt":"2021-09-01T10:31:09.414Z", + "updatedAt":"2022-08-08T14:12:07.974Z", + "connections":[ + { + "id":"20NBaKc9N7u13PPOVb3CNfozNvo", + "sourceId":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "destinationId":"20NBa9wa4Zb5ZHkJHO2IEiw8eWl", + "enabled":true, + "deleted":false, + "createdAt":"2021-11-02T17:47:06.381Z", + "updatedAt":"2021-11-02T17:47:06.381Z" + }, + { + "id":"20Pa0p59biSDa8y0cgjLh7JTP05", + "sourceId":"1xXCuf5lPxC0FjFeZD3udJjYY98", + "destinationId":"20Pa0T3VoQZZYPgh5LQS2vuwg3N", + "enabled":true, + "deleted":false, + "createdAt":"2021-11-03T14:07:35.341Z", + "updatedAt":"2021-11-03T14:07:35.341Z" + } + ], + "destinations":[ + { + "config":{ + + }, + "liveEventsConfig":null, + "secretConfig":{ + + }, + "id":"20NBa9wa4Zb5ZHkJHO2IEiw8eWl", + "name":"androidTest-firebase", + "enabled":true, + "workspaceId":"1xXCSVsmPjqFKhRlpajnDXkGwYX", + "deleted":false, + "createdAt":"2021-11-02T17:47:05.153Z", + "updatedAt":"2022-05-25T11:34:17.873Z", + "revisionId":"29efQiNb1Cwh7GO9dfuuFZ7UN3Q", + "secretVersion":null, + "transformations":[ + + ], + "destinationDefinition":{ + "config":{ + "destConfig":{ + "ios":[ + "useNativeSDK" + ], + "unity":[ + "useNativeSDK" + ], + "android":[ + "useNativeSDK" + ], + "reactnative":[ + "useNativeSDK" + ], + "defaultConfig":[ + "blacklistedEvents", + "whitelistedEvents", + "eventFilteringOption" + ] + }, + "secretKeys":[ + + ], + "excludeKeys":[ + + ], + "includeKeys":[ + "blacklistedEvents", + "whitelistedEvents", + "eventFilteringOption" + ], + "transformAt":"processor", + "transformAtV1":"processor", + "supportedSourceTypes":[ + "android", + "ios", + "unity", + "reactnative" + ], + "saveDestinationResponse":false + }, + "configSchema":null, + "responseRules":null, + "options":null, + "id":"1YL4j4RpSLloVaMwKrOoXLfiryj", + "name":"FIREBASE", + "displayName":"Firebase", + "category":null, + "createdAt":"2020-02-26T09:17:52.231Z", + "updatedAt":"2022-05-13T09:31:26.459Z" + }, + "areTransformationsConnected":true + } + ], + "sourceDefinition":{ + "options":null, + "id":"1QGzOQGVLM35GgtteFH1vYCE0WT", + "name":"Android", + "displayName":"Android", + "category":null, + "createdAt":"2019-09-02T08:08:08.373Z", + "updatedAt":"2020-06-18T11:54:00.449Z" + } + } +} diff --git a/dependencies.gradle b/dependencies.gradle index d4660cbd9..928fc51e9 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -14,7 +14,7 @@ ext { versions = [ - work : "2.7.0", + work : "2.9.0", gson : "2.8.8", jackson : "2.12.4", jacksonKotlin : "2.13.3", @@ -26,14 +26,16 @@ ext { androidXTest : "1.4.0", androidXTestExtJunitKtx: "1.1.3", mockito : "1.10.19", - mockk : "1.12.7", + mockk : "1.13.3", junit : "4.+", awaitility : "4.1.0", - robolectric : "4.10.3", + robolectric : "4.12.1", jsonAssert : "1.5.0", mockito_kotlin : "4.0.0", mockito_inline : "4.7.0", + mockito_android : "5.8.0", androidXTestRunner : "1.4.0", + navigationRuntime : "2.7.5", ] deps = [ @@ -60,15 +62,19 @@ ext { jsonAssert : "org.skyscreamer:jsonassert:${versions.jsonAssert}", mockito_kotlin : "org.mockito.kotlin:mockito-kotlin:${versions.mockito_kotlin}", mockito_inline : "org.mockito:mockito-inline:${versions.mockito_inline}", + mockito_android : "org.mockito:mockito-android:${versions.mockito_android}", mockk : "io.mockk:mockk:${versions.mockk}", mockk_agent_jvm : "io.mockk:mockk-agent-jvm:${versions.mockk}", workTest : "androidx.work:work-testing:${versions.work}", androidXTestRunner : "androidx.test:runner:${versions.androidXTestRunner}", + navigationFragment : "androidx.navigation:navigation-fragment-ktx:${versions.navigationFragment}", + navigationRuntime : "androidx.navigation:navigation-runtime:${versions.navigationRuntime}", ] projects = [ core : ':core', + android : ':android', models : ':models', json_rudder_adapter : ':rudderjsonadapter', jackson_rudder_adapter: ':jacksonrudderadapter', @@ -77,11 +83,12 @@ ext { repository : ':repository', web : ':web', metrics : ':metrics', + test_common : ':libs:test-common', ] library = [ min_sdk : 19, - target_sdk : 33, + target_sdk : 34, version_name: '"2.0"', version_code: 1 ] -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 372bebaad..4bfdbe1df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,28 +1,31 @@ +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html # -# Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM -# Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain a -# copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Fri Apr 26 12:10:37 IST 2024 GROUP=com.rudderstack.android.sdk +POM_DEVELOPER_ID=Rudderstack +POM_DEVELOPER_NAME=Rudderstack, Inc. +POM_LICENCE_DIST=repo +POM_LICENCE_NAME=The MIT License (MIT) +POM_LICENCE_URL=http\://opensource.org/licenses/MIT +POM_SCM_CONNECTION="scm\:git\:git\://github.com/rudderlabs/rudder-sdk-android.git" +POM_SCM_DEV_CONNECTION="scm\:git\:ssh\://github.com\:rudderlabs/rudder-sdk-android.git" +POM_SCM_URL="https\://github.com/rudderlabs/rudder-sdk-android/tree/master-v2" +POM_URL="https\://github.com/rudderlabs/rudder-sdk-android" +VERSION_NAME=1.0.0 +android.defaults.buildfeatures.buildconfig=true android.enableJetifier=true +android.nonFinalResIds=false +android.nonTransitiveRClass=false android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -POM_URL="https://github.com/rudderlabs/rudder-sdk-android" -POM_SCM_URL="https://github.com/rudderlabs/rudder-sdk-android/tree/master-v2" -POM_SCM_CONNECTION="scm:git:git://github.com/rudderlabs/rudder-sdk-android.git" -POM_SCM_DEV_CONNECTION="scm:git:ssh://github.com:rudderlabs/rudder-sdk-android.git" -POM_LICENCE_NAME=The MIT License (MIT) -POM_LICENCE_URL=http://opensource.org/licenses/MIT -POM_LICENCE_DIST=repo -POM_DEVELOPER_ID=Rudderstack -POM_DEVELOPER_NAME=Rudderstack, Inc. -VERSION_NAME=1.0.0 \ No newline at end of file +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx1536M" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..348acd13a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,58 @@ +[versions] +android-core-ktx = "1.10.1" +gradle = "8.2.2" +kotlin = "1.8.22" +mockk = "1.13.3" +work = "2.9.0" +gson = "2.8.8" +jackson-core = "2.13.3" +jackson-module = "2.13.3" +moshi = "1.13.0" +mockito-kotlin = "4.0.0" +mockito = "4.0.0" +android-x-test = "1.4.0" +android-x-testrules = "1.4.0" +android-x-test-ext-junitktx = "1.1.3" +android-x-testrunner = "1.4.0" +android-x-annotation = "1.6.0" +android-x-test-espresso = "3.5.1" +hamcrest = "2.2" +awaitility = "4.1.0" +robolectric = "4.12.1" +junit = "4.+" +json-assert = "1.5.0" +gradle-nexus-publish = "1.3.0" + +[libraries] +android-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "android-core-ktx" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockk-agent = { group = "io.mockk", name = "mockk-agent-jvm", version.ref = "mockk" } +work = { group = "androidx.work", name = "work-runtime", version.ref = "work" } +work-multiprocess = { group = "androidx.work", name = "work-multiprocess", version.ref = "work" } +work-test = { group = "androidx.work", name = "work-testing", version.ref = "work" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson-core" } +jackson-module = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-module" } +moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } +moshi-adapters = { group = "com.squareup.moshi", name = "moshi-adapters", version.ref = "moshi" } +moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" } +mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockito-kotlin" } +mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +android-x-test = { group = "androidx.test", name = "core", version.ref = "android-x-test" } +android-x-testrules = { group = "androidx.test", name = "rules", version.ref = "android-x-testrules" } +android-x-test-ext-junitktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "android-x-test-ext-junitktx" } +android-x-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "android-x-test-espresso" } +android-x-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "android-x-annotation" } +android-x-testrunner = { group = "androidx.test", name = "runner", version.ref = "android-x-testrunner" } +hamcrest = { group = "org.hamcrest", name = "hamcrest", version.ref = "hamcrest" } +awaitility = { group = "org.awaitility", name = "awaitility", version.ref = "awaitility" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +json-assert = { group = "org.skyscreamer", name = "jsonassert", version.ref = "json-assert" } + +[plugins] +gradle-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "gradle-nexus-publish" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle index a16a04075..bb4a6476c 100644 --- a/gradle/versioning.gradle +++ b/gradle/versioning.gradle @@ -6,7 +6,6 @@ def getVersionName() { // If not release build add SNAPSHOT suffix return getVersionName(VERSION_NAME) } def getVersionName(version) { // If not release build add SNAPSHOT suffix - println("versioning call:$version") return isReleaseBuild() ? version : version+"-SNAPSHOT" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f..943f0cbfa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bfd2b60bf..6b67b7a89 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gsonrudderadapter/build.gradle b/gsonrudderadapter/build.gradle deleted file mode 100644 index 68791d9fe..000000000 --- a/gsonrudderadapter/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -compileKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} - -apply from : "$projectDir/../dependencies.gradle" - - - -dependencies { - - implementation(project(path: ":rudderjsonadapter")) - api deps.gson - - testImplementation deps.hamcrest - testImplementation 'junit:junit:4.+' -} -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/gsonrudderadapter/build.gradle.kts b/gsonrudderadapter/build.gradle.kts new file mode 100644 index 000000000..50f439a95 --- /dev/null +++ b/gsonrudderadapter/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("java-library") + id("kotlin") +} + +dependencies { + implementation(project(":rudderjsonadapter")) + api(libs.gson) + testImplementation(libs.junit) + testImplementation(libs.hamcrest) + +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-jar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/gsonrudderadapter/src/androidTest/java/com/rudderstack/android/gsonrudderadapter/ExampleInstrumentedTest.kt b/gsonrudderadapter/src/androidTest/java/com/rudderstack/android/gsonrudderadapter/ExampleInstrumentedTest.kt deleted file mode 100644 index 7deb061c9..000000000 --- a/gsonrudderadapter/src/androidTest/java/com/rudderstack/android/gsonrudderadapter/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.rudderstack.android.gsonrudderadapter - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rudderstack.gsonrudderadapter", appContext.packageName) - } -} diff --git a/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/GsonAdapter.kt b/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/GsonAdapter.kt index 919196ea6..fc984981d 100644 --- a/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/GsonAdapter.kt +++ b/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/GsonAdapter.kt @@ -20,7 +20,6 @@ import com.google.gson.reflect.TypeToken import com.rudderstack.rudderjsonadapter.JsonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter - /** * @see JsonAdapter * @@ -30,7 +29,7 @@ import com.rudderstack.rudderjsonadapter.RudderTypeAdapter class GsonAdapter(private val gson: Gson = GsonBuilder().create()) : JsonAdapter { override fun readJson(json: String, typeAdapter: RudderTypeAdapter): T? { - return gson.fromJson(json, typeAdapter.type?:object: TypeToken(){}.type) + return gson.fromJson(json, typeAdapter.type ?: object : TypeToken() {}.type) } override fun writeToJson(obj: T): String? { @@ -44,11 +43,10 @@ class GsonAdapter(private val gson: Gson = GsonBuilder().create()) : JsonAdapter override fun readMap(map: Map, resultClass: Class): T? { val jsonElement = gson.toJsonTree(map) return gson.fromJson(jsonElement, resultClass) - } override fun readJson(json: String, resultClass: Class): T { - //gson is comfortable in parsing just strings/primitives + // gson is comfortable in parsing just strings/primitives return gson.fromJson(json, resultClass) } -} \ No newline at end of file +} diff --git a/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/TypeAdapters.kt b/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/TypeAdapters.kt index 23aad3415..de6cd01c3 100644 --- a/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/TypeAdapters.kt +++ b/gsonrudderadapter/src/main/java/com/rudderstack/gsonrudderadapter/TypeAdapters.kt @@ -15,4 +15,3 @@ * Custom type adapters to be mentioned here */ package com.rudderstack.gsonrudderadapter - diff --git a/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ExampleUnitTest.kt b/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ExampleUnitTest.kt index 5c916d243..a1613d99f 100644 --- a/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ExampleUnitTest.kt +++ b/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ExampleUnitTest.kt @@ -1,17 +1,3 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - package com.rudderstack.gsonrudderadapter import com.google.gson.Gson @@ -28,18 +14,19 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } + @Test public fun whenDeserializingToSimpleObject_thenCorrect() { - val json = "{\"intValue\":\"2\",\"stringValue\":\"one\"}"; + val json = "{\"intValue\":\"2\",\"stringValue\":\"one\"}" val targetObject = Gson().fromJson(json, Foo::class.java) println("int value: ${targetObject.intValue}") - assertEquals(targetObject.intValue, 2); - assertEquals(targetObject.stringValue, "one"); + assertEquals(targetObject.intValue, 2) + assertEquals(targetObject.stringValue, "one") } class Foo { var intValue = 0 var stringValue: String? = null // + standard equals and hashCode implementations } -} \ No newline at end of file +} diff --git a/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ParsingTest.kt b/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ParsingTest.kt index 467403704..f15719786 100644 --- a/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ParsingTest.kt +++ b/gsonrudderadapter/src/test/java/com/rudderstack/gsonrudderadapter/ParsingTest.kt @@ -23,52 +23,56 @@ class ParsingTest { data class SomeClass(val name: String, val prop: String) val someJson = "{" + - "\"type1\" : [" + - "{" + - "\"name\":\"ludo\"," + - "\"prop\":\"iok\"" + - "}" + - "]" + - "}" - //for checking map conversion - data class MapClass(val name: String, val age : Int) + "\"type1\" : [" + + "{" + + "\"name\":\"ludo\"," + + "\"prop\":\"iok\"" + + "}" + + "]" + + "}" + // for checking map conversion + data class MapClass(val name: String, val age: Int) @Test fun checkDeserialization() { // val type = Map::class.java.typeName val rta = object : RudderTypeAdapter>>() {} val ja = GsonAdapter() - val res = ja.readJson>>( someJson, rta) + val res = ja.readJson>>(someJson, rta) assert(res != null) assert(res!!["type1"] != null) - assert(res["type1"]?.size?:0 ==1) + assert(res["type1"]?.size ?: 0 == 1) assert(res["type1"]?.get(0)?.name == "ludo") assert(res["type1"]?.get(0)?.prop == "iok") - } + @Test - fun checkSerialization(){ + fun checkSerialization() { val someClass = SomeClass("ludo", "iok") val ja = GsonAdapter() - val res = ja.writeToJson>>(mapOf(Pair("type1", listOf(someClass)) ), - object : RudderTypeAdapter>>(){}) - assert(res == someJson.replace(" ","")) + val res = ja.writeToJson>>( + mapOf(Pair("type1", listOf(someClass))), + object : RudderTypeAdapter>>() {}, + ) + assert(res == someJson.replace(" ", "")) } @Test - fun checkMapToObjConversion(){ + fun checkMapToObjConversion() { val mapRepresentation = mapOf("name" to "Foo", "age" to 20) val adapter = GsonAdapter() - val outCome : MapClass? = adapter.readMap(mapRepresentation, MapClass::class.java) + val outCome: MapClass? = adapter.readMap(mapRepresentation, MapClass::class.java) - MatcherAssert.assertThat(outCome, Matchers.allOf( - Matchers.notNullValue(), - Matchers.isA(MapClass::class.java), - Matchers.hasProperty("name", Matchers.equalTo("Foo")), - Matchers.hasProperty("age", Matchers.equalTo(20)) - )) + MatcherAssert.assertThat( + outCome, + Matchers.allOf( + Matchers.notNullValue(), + Matchers.isA(MapClass::class.java), + Matchers.hasProperty("name", Matchers.equalTo("Foo")), + Matchers.hasProperty("age", Matchers.equalTo(20)), + ), + ) } - -} \ No newline at end of file +} diff --git a/jacksonrudderadapter/build.gradle b/jacksonrudderadapter/build.gradle deleted file mode 100644 index dfa280e9e..000000000 --- a/jacksonrudderadapter/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -compileKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -apply from : "$projectDir/../dependencies.gradle" - - -dependencies { - - implementation(project(path: ":rudderjsonadapter")) - api deps.jackson - api deps.jacksonKotlin - - testImplementation deps.hamcrest - testImplementation 'junit:junit:4.+' -} -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/jacksonrudderadapter/build.gradle.kts b/jacksonrudderadapter/build.gradle.kts new file mode 100644 index 000000000..9d891d9be --- /dev/null +++ b/jacksonrudderadapter/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("java-library") + id("kotlin") +} + +dependencies { + implementation(project(":rudderjsonadapter")) + api(libs.jackson.core) + api(libs.jackson.module) + testImplementation(libs.junit) + testImplementation(libs.hamcrest) + +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-jar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/jacksonrudderadapter/src/main/java/com/rudderstack/jacksonrudderadapter/JacksonAdapter.kt b/jacksonrudderadapter/src/main/java/com/rudderstack/jacksonrudderadapter/JacksonAdapter.kt index 2ff6749fb..dcf1f9112 100644 --- a/jacksonrudderadapter/src/main/java/com/rudderstack/jacksonrudderadapter/JacksonAdapter.kt +++ b/jacksonrudderadapter/src/main/java/com/rudderstack/jacksonrudderadapter/JacksonAdapter.kt @@ -15,10 +15,10 @@ package com.rudderstack.jacksonrudderadapter import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.rudderstack.rudderjsonadapter.JsonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter -import com.fasterxml.jackson.databind.DeserializationFeature import java.lang.reflect.Type /** @@ -56,12 +56,11 @@ class JacksonAdapter : JsonAdapter { } override fun readMap(map: Map, resultClass: Class): T? { - return objectMapper.convertValue(map, resultClass) } override fun readJson(json: String, resultClass: Class): T { - //in case T is primitive, json needs to be returned as primitive + // in case T is primitive, json needs to be returned as primitive return when (resultClass) { String::class.java, CharSequence::class.java -> json as T @@ -70,7 +69,6 @@ class JacksonAdapter : JsonAdapter { Float::class.java -> json.toFloat() as T Long::class.java -> json.toLong() as T else -> objectMapper.readValue(json, resultClass) - } } -} \ No newline at end of file +} diff --git a/jacksonrudderadapter/src/test/java/com/rudderstack/jacksonrudderadapter/ParsingTest.kt b/jacksonrudderadapter/src/test/java/com/rudderstack/jacksonrudderadapter/ParsingTest.kt index 56e64df20..2a56dd8ab 100644 --- a/jacksonrudderadapter/src/test/java/com/rudderstack/jacksonrudderadapter/ParsingTest.kt +++ b/jacksonrudderadapter/src/test/java/com/rudderstack/jacksonrudderadapter/ParsingTest.kt @@ -25,22 +25,24 @@ class ParsingTest { @JsonProperty("name") val name: String, @JsonProperty("prop") - val prop: String) + val prop: String, + ) val someJson = "{" + - "\"type1\" : [" + - "{" + - "\"name\":\"ludo\"," + - "\"prop\":\"iok\"" + - "}" + - "]" + - "}" - - //for checking map conversion - data class MapClass(@JsonProperty("name") val name: String, - @JsonProperty("age") - val age: Int) + "\"type1\" : [" + + "{" + + "\"name\":\"ludo\"," + + "\"prop\":\"iok\"" + + "}" + + "]" + + "}" + // for checking map conversion + data class MapClass( + @JsonProperty("name") val name: String, + @JsonProperty("age") + val age: Int, + ) @Test fun checkDeserialization() { @@ -54,7 +56,6 @@ class ParsingTest { assert(res["type1"]?.size ?: 0 == 1) assert(res["type1"]?.get(0)?.name == "ludo") assert(res["type1"]?.get(0)?.prop == "iok") - } @Test @@ -62,8 +63,10 @@ class ParsingTest { val someClass = SomeClass("ludo", "iok") val ja = JacksonAdapter() val res = - ja.writeToJson>>(mapOf(Pair("type1", listOf(someClass))), - object : RudderTypeAdapter>>() {}) + ja.writeToJson>>( + mapOf(Pair("type1", listOf(someClass))), + object : RudderTypeAdapter>>() {}, + ) // println(res) assert(res == someJson.replace(" ", "")) } @@ -76,13 +79,13 @@ class ParsingTest { val outCome: MapClass? = adapter.readMap(mapRepresentation, MapClass::class.java) MatcherAssert.assertThat( - outCome, Matchers.allOf( + outCome, + Matchers.allOf( Matchers.notNullValue(), Matchers.isA(MapClass::class.java), Matchers.hasProperty("name", Matchers.equalTo("Foo")), - Matchers.hasProperty("age", Matchers.equalTo(20)) - ) + Matchers.hasProperty("age", Matchers.equalTo(20)), + ), ) } - -} \ No newline at end of file +} diff --git a/libs/navigationplugin/.gitignore b/libs/navigationplugin/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/libs/navigationplugin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/navigationplugin/build.gradle.kts b/libs/navigationplugin/build.gradle.kts new file mode 100644 index 000000000..e9d2864aa --- /dev/null +++ b/libs/navigationplugin/build.gradle.kts @@ -0,0 +1,68 @@ +/* + * Creator: Debanjan Chatterjee on 20/11/23, 4:02 pm Last modified: 20/11/23, 4:02 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} +val dependencyPath = "${project.projectDir.parentFile.parent}/dependencies.gradle" +apply(from = dependencyPath) +val deps: HashMap by extra +val library: HashMap by extra +val projects: HashMap by extra + +android { + compileSdk = library["target_sdk"] as Int + + defaultConfig { + minSdk = library["min_sdk"] as Int +// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + //for code access + buildConfigField("String", "LIBRARY_VERSION_NAME", library["version_name"] as String) + } + buildFeatures { + buildConfig = true + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + + } + kotlinOptions { + jvmTarget = RudderstackBuildConfig.Build.JAVA_VERSION.toString() + javaParameters = true + } + namespace = "com.rudderstack.android.navigationplugin" +} + +dependencies { + implementation(deps["kotlinCore"].toString()) + compileOnly(project(projects["android"].toString())) + compileOnly(deps["navigationRuntime"].toString()) +} +tasks.withType(type = org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs::class) { + kotlinOptions.jvmTarget = "17" +} +apply(from = "${project.projectDir.parentFile.parent}/gradle/artifacts-aar.gradle") +apply(from = "${project.projectDir.parentFile.parent}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile.parent}/gradle/codecov.gradle") diff --git a/libs/navigationplugin/config.properties b/libs/navigationplugin/config.properties new file mode 100644 index 000000000..554c2420d --- /dev/null +++ b/libs/navigationplugin/config.properties @@ -0,0 +1 @@ +platform="java-kotlin" diff --git a/libs/navigationplugin/gradle.properties b/libs/navigationplugin/gradle.properties new file mode 100644 index 000000000..a3f27135f --- /dev/null +++ b/libs/navigationplugin/gradle.properties @@ -0,0 +1,7 @@ +POM_ARTIFACT_ID=navigation +GROUP=com.rudderstack.android +POM_PACKAGING=aar +VERSION_CODE=1 +VERSION_NAME=1.0.0 +POM_NAME=Rudderstack navigation SDK for Android +POM_DESCRIPTION=Rudderstack navigation SDK for Android \ No newline at end of file diff --git a/libs/navigationplugin/package-lock.json b/libs/navigationplugin/package-lock.json new file mode 100644 index 000000000..558cd88d0 --- /dev/null +++ b/libs/navigationplugin/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "navigation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "navigation", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/libs/navigationplugin/package.json b/libs/navigationplugin/package.json new file mode 100644 index 000000000..ef6b9d154 --- /dev/null +++ b/libs/navigationplugin/package.json @@ -0,0 +1,11 @@ +{ + "name": "navigation", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/libs/navigationplugin/proguard-rules.pro b/libs/navigationplugin/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/libs/navigationplugin/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/libs/navigationplugin/project.json b/libs/navigationplugin/project.json new file mode 100644 index 000000000..b65db7c30 --- /dev/null +++ b/libs/navigationplugin/project.json @@ -0,0 +1,81 @@ +{ + "name": "navigation", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "./libs/navigationplugin/src", + "targets": { + "build": { + "executor": "@jnxplus/nx-gradle:build", + "dependsOn": [ + { + "projects": [ + "android", + "rudderreporter" + ], + "target": "build" + } + ], + "outputs": [ + "{projectRoot}/libs/navigationplugin/build" + ] + }, + "lint": { + "dependsOn": [ + "build" + ], + "executor": "@jnxplus/nx-gradle:lint", + "options": { + "linter": "ktlint" + } + }, + "test": { + "dependsOn": [ + "build" + ], + "executor": "@jnxplus/nx-gradle:test" + }, + "ktformat": { + "executor": "@jnxplus/nx-gradle:ktformat" + }, + "version": { + "executor": "@jscutlery/semver:version", + "options": { + "baseBranch": "master", + "preset": "conventional", + "tagPrefix": "${projectName}@" + } + }, + "sync-bumped-version-properties": { + "executor": "nx:run-commands", + "options": { + "command": "node gradle-updater.js 'libs/navigationplugin'" + } + }, + "github": { + "executor": "@jscutlery/semver:github", + "options": { + "tag": "navigation@1.0.0", + "notesFile": ".libs/navigationplugin/CHANGELOG_LATEST.md" + } + }, + "release-sonatype": { + "dependsOn": [ + "build" + ], + "executor": "nx:run-commands", + "options": { + "command": "echo 'navigation-release' && ./gradlew :libs:navigationplugin:publishToSonatype -Prelease closeAndReleaseSonatypeStagingRepository" + } + }, + "snapshot-release": { + "dependsOn": [ + "build" + ], + "executor": "nx:run-commands", + "options": { + "command": "echo 'navigation-snapshot' && ./gradlew :libs:navigationplugin:publishToSonatype" + } + } + }, + "tags": [] +} diff --git a/libs/navigationplugin/src/main/AndroidManifest.xml b/libs/navigationplugin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8cba1b2b4 --- /dev/null +++ b/libs/navigationplugin/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/Lifecycle.kt b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/Lifecycle.kt new file mode 100644 index 000000000..7a977991f --- /dev/null +++ b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/Lifecycle.kt @@ -0,0 +1,89 @@ +/* + * Creator: Debanjan Chatterjee on 20/11/23, 6:04 pm Last modified: 20/11/23, 4:02 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.rudderstack.android.navigationplugin + +import androidx.navigation.NavController +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.android.navigationplugin.internal.NavControllerState +import com.rudderstack.android.navigationplugin.internal.NavigationPlugin +import com.rudderstack.core.Analytics + +/** + * Lifecycle events to be used for tracking app lifecycle events + */ +private val navControllerState: NavControllerState by lazy { + NavControllerState() +} +private var navigationPlugin: NavigationPlugin? = null + +/** + * Tracks lifecycle events for the given [NavController] + * example code for Compose navigation: + * ``` + * @Composable + * fun SunflowerApp() { + * val navController = rememberNavController() + * LaunchedEffect("first_launch") { + * trackLifecycle(navController) + * } + * SunFlowerNavHost( + * navController = navController + * ) + * } + * ``` + * Example code for Fragment navigation: + * ``` + * override fun onCreate(savedInstanceState: Bundle?) { + * super.onCreate(savedInstanceState) + * + * val binding = ActivityMainBinding.inflate(layoutInflater) + * setContentView(binding.root) + * + * // Get the navigation host fragment from this Activity + * val navHostFragment = supportFragmentManager + * .findFragmentById(R.id.nav_host_fragment) as NavHostFragment + * // Instantiate the navController using the NavHostFragment + * navController = navHostFragment.navController + * trackLifecycle(navController) + * } + * ``` + * In case multiple [NavController]s are used, call this method for each of them. + * To stop tracking lifecycle events for a [NavController], call [removeLifecycleTracking] + * + * @param navController : [NavController] to be tracked + */ +fun Analytics.trackLifecycle(navController: NavController) { + if(currentConfigurationAndroid?.recordScreenViews != true || + currentConfigurationAndroid?.trackLifecycleEvents != true) return + + navControllerState.update( + navControllerState.value?.plus(navController)?: setOf(navController) + ) + if (navigationPlugin == null) { + navigationPlugin = NavigationPlugin(navControllerState).also { + addInfrastructurePlugin(it) + } + } +} + +/** + * To stop tracking lifecycle events for a [NavController], call this method. + * + * @param navController : [NavController] to be removed from tracking + */ +fun Analytics.removeLifecycleTracking(navController: NavController) { + navControllerState.update( + navControllerState.value?.minus(navController) + ) +} \ No newline at end of file diff --git a/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavControllerState.kt b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavControllerState.kt new file mode 100644 index 000000000..544cffe93 --- /dev/null +++ b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavControllerState.kt @@ -0,0 +1,20 @@ +/* + * Creator: Debanjan Chatterjee on 20/11/23, 9:03 pm Last modified: 20/11/23, 9:03 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.navigationplugin.internal + +import androidx.navigation.NavController +import com.rudderstack.core.State + +class NavControllerState : State>() {} \ No newline at end of file diff --git a/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavigationPlugin.kt b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavigationPlugin.kt new file mode 100644 index 000000000..e921f1598 --- /dev/null +++ b/libs/navigationplugin/src/main/java/com/rudderstack/android/navigationplugin/internal/NavigationPlugin.kt @@ -0,0 +1,139 @@ +/* + * Creator: Debanjan Chatterjee on 20/11/23, 8:55 pm Last modified: 20/11/23, 6:29 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.navigationplugin.internal + +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import com.rudderstack.android.LifecycleListenerPlugin +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin +import com.rudderstack.core.State + +internal class NavigationPlugin( + private val navControllerState: State> +) : InfrastructurePlugin, NavController.OnDestinationChangedListener { + + override lateinit var analytics: Analytics + + private var currentNavControllers: Collection? = null + private val currentConfig + get() = analytics?.currentConfigurationAndroid + + override fun setup(analytics: Analytics) { + super.setup(analytics) + if (currentConfig?.trackLifecycleEvents == true && currentConfig?.recordScreenViews == true) { + navControllerState.subscribe { newValue, _ -> updateNavControllers(newValue) } + } + } + + private fun updateNavControllers(updatedNavControllers: Collection?) { + synchronized(this) { + removeDeletedNavControllers(updatedNavControllers) + setupAddedNavControllers(updatedNavControllers) + currentNavControllers = updatedNavControllers + } + } + + + private fun setupAddedNavControllers(updatedNavControllers: Collection?) { + updatedNavControllers?.let { + val addedNavControllers: List = + it.minus((currentNavControllers ?: emptyList()).toSet()) + addedNavControllers.forEach { navController -> + navController.addOnDestinationChangedListener(this) + } + } + } + + private fun removeDeletedNavControllers(updatedNavControllers: Collection?) { + currentNavControllers?.let { + val deletedNavControllers: List = + it.minus((updatedNavControllers ?: emptyList()).toSet()) + deletedNavControllers.forEach { navController -> + navController.removeOnDestinationChangedListener(this) + } + } + } + + override fun shutdown() { + updateNavControllers(listOf()) + } + + @Synchronized + override fun onDestinationChanged( + controller: NavController, + destination: NavDestination, + arguments: Bundle? + ) { + when (destination.navigatorName) { + "fragment" -> { + trackFragmentScreenView(destination, arguments) + } + + "composable" -> { + trackComposableScreenView(destination, arguments) + } + } + } + + private fun trackComposableScreenView( + destination: NavDestination, + arguments: Bundle? + ) { + if (currentConfig?.trackLifecycleEvents != true || currentConfig?.recordScreenViews != true) return + val argumentKeys = destination.arguments.keys + val screenName = destination.route?.let { + if (argumentKeys.isEmpty()) it + else { + val argumentsIndex = it.indexOf('/') + if (argumentsIndex == -1) it + else it.substring(0, argumentsIndex) + } + }.toString() + broadcastScreenChange(screenName, getProperties(arguments, argumentKeys)) + } + + private fun trackFragmentScreenView(destination: NavDestination, arguments: Bundle?) { + if (currentConfig?.trackLifecycleEvents != true || currentConfig?.recordScreenViews != true) return + val screenName = destination.label.toString() + val properties = getProperties(arguments, destination.arguments.keys) + broadcastScreenChange(screenName, properties) + } + + private fun getProperties( + arguments: Bundle?, argumentKeys: Set + ): Map = arguments?.let { bundle -> + argumentKeys.associateWith { bundle.get(it) } + }?.filter { it.value != null }?.mapValues { it.value!! } ?: mapOf() + + private fun broadcastScreenChange( + screenName: String, + properties: Map? + ) { + analytics.applyInfrastructureClosure { + if (this is LifecycleListenerPlugin) { + this.onScreenChange(screenName, properties) + } + } + analytics.applyMessageClosure { + if (this is LifecycleListenerPlugin) { + this.onScreenChange(screenName, properties) + } + } + } + +} diff --git a/libs/sync/.gitignore b/libs/sync/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/libs/sync/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/sync/build.gradle.kts b/libs/sync/build.gradle.kts new file mode 100644 index 000000000..3a6695cd7 --- /dev/null +++ b/libs/sync/build.gradle.kts @@ -0,0 +1,95 @@ +/* + * Creator: Debanjan Chatterjee on 18/03/24, 11:44 am Last modified: 18/03/24, 11:44 am + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} +val dependencyPath = "${project.projectDir.parentFile.parent}/dependencies.gradle" +apply(from = dependencyPath) +val deps: HashMap by extra +val library: HashMap by extra +val projects: HashMap by extra + +android { + compileSdk = library["target_sdk"].toString().toInt() + + defaultConfig { + minSdk = library["min_sdk"].toString().toInt() + consumerProguardFiles("consumer-rules.pro") + + //for code access + buildConfigField("String", "LIBRARY_VERSION_NAME", library["version_name"] as String) + } + buildFeatures { + buildConfig = true + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + javaParameters = true + } + testOptions { + unitTests { + this.isIncludeAndroidResources = true + } + } + namespace = "com.rudderstack.android.sync" +} + +dependencies { + implementation(deps["kotlinCore"].toString()) + compileOnly(project(":core")) + compileOnly(project(":android")) + //dependency on work manager + implementation(deps["work"].toString()) + implementation(deps["workMultiprocess"].toString()) + + testImplementation(project(":core")) + testImplementation(project(":android")) + testImplementation(deps["workTest"].toString()) + testImplementation(deps["androidXTest"].toString()) + testImplementation(deps["hamcrest"].toString()) + testImplementation(deps["mockito"].toString()) + testImplementation(deps["mockito_kotlin"].toString()) + testImplementation(deps["mockk"].toString()) + testImplementation(deps["mockk_agent_jvm"].toString()) + testImplementation(deps["awaitility"].toString()) + testImplementation(deps["robolectric"].toString()) + testImplementation(deps["androidXTestExtJunitKtx"].toString()) + testImplementation(deps["androidXTestRules"].toString()) + testImplementation(deps["junit"].toString()) + + + androidTestImplementation(deps["androidXTestExtJunitKtx"].toString()) +} +tasks.withType(type = org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs::class) { + kotlinOptions.jvmTarget = "1.8" +} + +apply(from = "${project.projectDir.parentFile.parent}/gradle/artifacts-aar.gradle") +apply(from = "${project.projectDir.parentFile.parent}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile.parent}/gradle/codecov.gradle") diff --git a/libs/sync/config.properties b/libs/sync/config.properties new file mode 100644 index 000000000..554c2420d --- /dev/null +++ b/libs/sync/config.properties @@ -0,0 +1 @@ +platform="java-kotlin" diff --git a/libs/sync/consumer-rules.pro b/libs/sync/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/libs/sync/gradle.properties b/libs/sync/gradle.properties new file mode 100644 index 000000000..eefed9296 --- /dev/null +++ b/libs/sync/gradle.properties @@ -0,0 +1,7 @@ +POM_ARTIFACT_ID=sync +GROUP=com.rudderstack.android +POM_PACKAGING=aar +VERSION_CODE=1 +VERSION_NAME=1.0.0 +POM_NAME=Rudderstack Synchronization SDK for Android +POM_DESCRIPTION=Rudderstack Synchronization SDK for Android using Work Manager \ No newline at end of file diff --git a/libs/sync/package-lock.json b/libs/sync/package-lock.json new file mode 100644 index 000000000..11441e5b5 --- /dev/null +++ b/libs/sync/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sync", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/models/package.json b/libs/sync/package.json similarity index 69% rename from models/package.json rename to libs/sync/package.json index 1303fbbf0..7a003486e 100644 --- a/models/package.json +++ b/libs/sync/package.json @@ -1,7 +1,8 @@ { - "name": "models", - "version": "0.1.0", + "name": "sync", + "version": "1.0.0", "description": "", + "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/libs/sync/proguard-rules.pro b/libs/sync/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/libs/sync/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/models/project.json b/libs/sync/project.json similarity index 62% rename from models/project.json rename to libs/sync/project.json index d95f29102..9a5b1d2cf 100644 --- a/models/project.json +++ b/libs/sync/project.json @@ -1,13 +1,22 @@ { - "name": "models", + "name": "sync", "$schema": "../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "sourceRoot": "./models/src", + "sourceRoot": "./libs/sync/src", "targets": { "build": { "executor": "@jnxplus/nx-gradle:build", + "dependsOn": [ + { + "projects": [ + "android", + "rudderreporter" + ], + "target": "build" + } + ], "outputs": [ - "{projectRoot}/models/build" + "{projectRoot}/libs/sync/build" ] }, "lint": { @@ -33,21 +42,20 @@ "options": { "baseBranch": "master", "preset": "conventional", - "tagPrefix": "{projectName}@" + "tagPrefix": "${projectName}@" } }, "sync-bumped-version-properties": { "executor": "nx:run-commands", "options": { - "command": "node gradle-updater.js 'models'" + "command": "node gradle-updater.js 'libs/sync'" } }, "github": { "executor": "@jscutlery/semver:github", "options": { - "title": "models@0.1.0", - "tag": "models@0.1.0", - "notesFile": "./models/CHANGELOG_LATEST.md" + "tag": "navigation@1.0.0", + "notesFile": ".libs/sync/CHANGELOG_LATEST.md" } }, "release-sonatype": { @@ -56,7 +64,7 @@ ], "executor": "nx:run-commands", "options": { - "command": "echo 'models-release' && cd models && sh ../gradlew generatePomFileForReleasePublication && ../gradlew publishToSonatype && cd .. && ./gradlew findSonatypeStagingRepository closeSonatypeStagingRepository && ./gradlew findSonatypeStagingRepository releaseSonatypeStagingRepository" + "command": "echo 'sync-release' && ./gradlew :libs:sync:publishToSonatype -Prelease closeAndReleaseSonatypeStagingRepository" } }, "snapshot-release": { @@ -65,7 +73,7 @@ ], "executor": "nx:run-commands", "options": { - "command": "echo 'models-snapshot' && cd models && sh ../gradlew generatePomFileForReleasePublication && ../gradlew publishToSonatype && cd .." + "command": "echo 'sync-snapshot' && ./gradlew :libs:sync:publishToSonatype" } } }, diff --git a/libs/sync/src/main/AndroidManifest.xml b/libs/sync/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7493ea647 --- /dev/null +++ b/libs/sync/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/libs/sync/src/main/java/com/rudderstack/android/sync/WorkManagerAnalyticsFactory.kt b/libs/sync/src/main/java/com/rudderstack/android/sync/WorkManagerAnalyticsFactory.kt new file mode 100644 index 000000000..b163635bd --- /dev/null +++ b/libs/sync/src/main/java/com/rudderstack/android/sync/WorkManagerAnalyticsFactory.kt @@ -0,0 +1,30 @@ +/* + * Creator: Debanjan Chatterjee on 27/11/23, 4:58 pm Last modified: 27/11/23, 4:58 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sync +import com.rudderstack.android.sync.internal.RudderSyncWorker +import android.app.Application +import com.rudderstack.core.Analytics + +/** + * Factory to create Analytics instance for WorkManager + * In case process is killed prior to initialization of [RudderSyncWorker] then + * [RudderSyncWorker] will create a new instance of Analytics using this factory + * This factory should have a no-arg constructor so that the instance can be created + * wth reflection + * + */ +interface WorkManagerAnalyticsFactory { + fun createAnalytics(application: Application): Analytics +} \ No newline at end of file diff --git a/libs/sync/src/main/java/com/rudderstack/android/sync/WorkerManagerPlugin.kt b/libs/sync/src/main/java/com/rudderstack/android/sync/WorkerManagerPlugin.kt new file mode 100644 index 000000000..5e1e5439b --- /dev/null +++ b/libs/sync/src/main/java/com/rudderstack/android/sync/WorkerManagerPlugin.kt @@ -0,0 +1,45 @@ +package com.rudderstack.android.sync + +import android.app.Application +import com.rudderstack.android.sync.internal.registerWorkManager +import com.rudderstack.android.sync.internal.unregisterWorkManager +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.core.Analytics +import com.rudderstack.core.InfrastructurePlugin + +abstract class WorkerManagerPlugin : InfrastructurePlugin { + + private var application: Application? = null + private var analyticsIdentifier: String? = null + + override fun setup(analytics: Analytics) { + super.setup(analytics) + analyticsIdentifier = analytics.writeKey + val currentConfig = analytics.currentConfigurationAndroid + if (currentConfig?.isPeriodicFlushEnabled != true) { + currentConfig?.logger?.error( + log = "Halting Worker manager plugin initialization since isPeriodicFlushEnabled configuration is false" + ) + return + } + currentConfig.apply { + this@WorkerManagerPlugin.application = this.application + application.registerWorkManager( + analytics, workManagerAnalyticsFactoryClassName + ) + } + } + + override fun shutdown() { + application?.unregisterWorkManager(analyticsIdentifier ?: return) + analyticsIdentifier = null + } + + /** + * Internal classes are not supported. + * This is because instantiating an inner class requires the parent class instance. + * It's not worth it to try finding an instance in Heap. Cause this approach might conflict + * with garbage collector + */ + abstract val workManagerAnalyticsFactoryClassName: Class +} diff --git a/libs/sync/src/main/java/com/rudderstack/android/sync/internal/DataSyncWorkManagerExtensions.kt b/libs/sync/src/main/java/com/rudderstack/android/sync/internal/DataSyncWorkManagerExtensions.kt new file mode 100644 index 000000000..0c955b719 --- /dev/null +++ b/libs/sync/src/main/java/com/rudderstack/android/sync/internal/DataSyncWorkManagerExtensions.kt @@ -0,0 +1,117 @@ +/* + * Creator: Debanjan Chatterjee on 18/03/24, 6:22 pm Last modified: 04/01/24, 5:47 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sync.internal + +import android.app.Application +import androidx.annotation.VisibleForTesting +import androidx.work.Configuration +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.multiprocess.RemoteWorkManager +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.android.sync.WorkManagerAnalyticsFactory +import com.rudderstack.core.Analytics +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +/** + * Aids in data syncing to server with help of work manager. + * Only works if work manager dependency is added externally + * Also, if multi-process support is needed, set the default process name. + * For multi-process support, the following dependency is expected + * Implementation "androidx.work:work-multiprocess:2.5.x" + */ +private const val WORK_MANAGER_TAG = "rudder_sync" +private const val WORK_NAME = "rudder_sync_work" +private const val REPEAT_INTERVAL_IN_MINS = 15L +private var analyticsRefMap = ConcurrentHashMap>() + +private fun Analytics.generateKeyForLabel(label: String) = addKeyToLabel(label, writeKey) +private fun addKeyToLabel(label: String, key: String) = "${label}_$key" +internal fun getAnalytics(key: String) = analyticsRefMap[key]?.get() + + +private val constraints by lazy { + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() +} + +private fun Analytics.syncWorker( + workManagerAnalyticsFactoryClassName: Class +) = PeriodicWorkRequestBuilder( + REPEAT_INTERVAL_IN_MINS, TimeUnit.MINUTES) + .setInitialDelay(REPEAT_INTERVAL_IN_MINS, TimeUnit.MINUTES) + .setConstraints(constraints) + .setInputData( + workerInputData(workManagerAnalyticsFactoryClassName)) + .addTag(generateKeyForLabel(WORK_MANAGER_TAG)).build() + +@VisibleForTesting +internal fun Analytics.workerInputData(workManagerAnalyticsFactoryClassName: Class) = + Data.Builder() + .putString( + WORKER_ANALYTICS_FACTORY_KEY, workManagerAnalyticsFactoryClassName.name) + .putString(WORKER_ANALYTICS_INSTANCE_KEY, writeKey) + .build() + +internal fun Application.registerWorkManager( + analytics: Analytics, workManagerAnalyticsFactoryClass: Class +) { + analytics.logger.debug(log = "Initializing work manager") + if (getAnalytics(analytics.writeKey)?.takeUnless { it.isShutdown } != null) { + analytics.logger.debug(log = "Work manager already initialized") + return + } + + analyticsRefMap[analytics.writeKey] = WeakReference(analytics) + + Configuration.Builder().also { + // if process name is available, this is a multi-process app + analytics.currentConfigurationAndroid?.defaultProcessName?.apply { + it.setDefaultProcessName(this) + } + analytics.currentConfigurationAndroid?.networkExecutor?.apply { + it.setExecutor(this) + } + } + if (analytics.currentConfigurationAndroid?.multiProcessEnabled == true) { + RemoteWorkManager.getInstance(this).enqueueUniquePeriodicWork( + analytics.generateKeyForLabel(WORK_NAME), + ExistingPeriodicWorkPolicy.KEEP, + analytics.syncWorker(workManagerAnalyticsFactoryClass) + ) + } else { + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + analytics.generateKeyForLabel(WORK_NAME), + ExistingPeriodicWorkPolicy.KEEP, + analytics.syncWorker(workManagerAnalyticsFactoryClass) + ) + } + +} + +fun Application.unregisterWorkManager(writeKey: String) { + analyticsRefMap[writeKey]?.clear() + analyticsRefMap.remove(writeKey) + WorkManager.getInstance(this) + .cancelAllWorkByTag(addKeyToLabel(WORK_MANAGER_TAG, writeKey)) + RemoteWorkManager.getInstance(this) + .cancelAllWorkByTag(addKeyToLabel(WORK_MANAGER_TAG, writeKey)) +} diff --git a/libs/sync/src/main/java/com/rudderstack/android/sync/internal/RudderSyncWorker.kt b/libs/sync/src/main/java/com/rudderstack/android/sync/internal/RudderSyncWorker.kt new file mode 100644 index 000000000..95737ab0c --- /dev/null +++ b/libs/sync/src/main/java/com/rudderstack/android/sync/internal/RudderSyncWorker.kt @@ -0,0 +1,65 @@ +/* + * Creator: Debanjan Chatterjee on 18/03/24, 6:20 pm Last modified: 18/03/24, 6:11 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sync.internal + +import android.app.Application +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.rudderstack.android.sync.WorkManagerAnalyticsFactory +import com.rudderstack.core.Analytics + + +internal const val WORKER_ANALYTICS_FACTORY_KEY = "WORKER_ANALYTICS_FACTORY_KEY" +internal const val WORKER_ANALYTICS_INSTANCE_KEY = "WORKER_ANALYTICS_INSTANCE_KEY" +/** + * Syncs the data at an interval with rudder server + * + */ +internal class RudderSyncWorker( + appContext: Context, workerParams: WorkerParameters +) : Worker(appContext, workerParams) { + override fun doWork(): Result { + (applicationContext as? Application)?.let { + val analyticsInstanceKey = + inputData.getString(WORKER_ANALYTICS_INSTANCE_KEY) ?: return Result.failure() + val weakSyncAnalytics = getAnalytics(analyticsInstanceKey) + if (weakSyncAnalytics?.isShutdown == true) { + weakSyncAnalytics.logger.warn(log = "Cannot do work. Analytics instance is " + + "already shutdown") + return Result.failure() + } + val syncAnalytics = (weakSyncAnalytics ?: createSyncAnalytics()) + val success = syncAnalytics?.blockingFlush() + syncAnalytics?.logger?.debug(log = "Data upload through worker. success: $success") + if (weakSyncAnalytics == null) { + syncAnalytics?.shutdown() + } + + return if (success == true) Result.success() else Result.failure() + } + return Result.failure() + } + + private fun createSyncAnalytics(): Analytics? { + val analyticsFactoryClassName = inputData.getString(WORKER_ANALYTICS_FACTORY_KEY) + return analyticsFactoryClassName?.let { + val analyticsFactory = Class.forName(it).getDeclaredConstructor().newInstance() as WorkManagerAnalyticsFactory + analyticsFactory.createAnalytics(applicationContext as Application) + } + + } + +} diff --git a/libs/sync/src/test/java/com/rudderstack/android/sync/RudderSyncWorkerTest.kt b/libs/sync/src/test/java/com/rudderstack/android/sync/RudderSyncWorkerTest.kt new file mode 100644 index 000000000..86b84fd8d --- /dev/null +++ b/libs/sync/src/test/java/com/rudderstack/android/sync/RudderSyncWorkerTest.kt @@ -0,0 +1,98 @@ +/* + * Creator: Debanjan Chatterjee on 18/03/24, 12:47 pm Last modified: 18/03/24, 12:13 pm + * Copyright: All rights reserved Ⓒ 2024 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sync + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker +import androidx.work.testing.TestWorkerBuilder +import com.rudderstack.android.ConfigurationAndroid +import com.rudderstack.android.utilities.currentConfigurationAndroid +import com.rudderstack.android.sync.internal.RudderSyncWorker +import com.rudderstack.android.sync.internal.workerInputData +import com.rudderstack.android.sync.utils.TestLogger +import com.rudderstack.core.Analytics +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [29]) +class RudderSyncWorkerTest { + @get:Rule + val mockkRule = MockKRule(this) + @MockK + lateinit var analytics: Analytics + @MockK + lateinit var configuration: ConfigurationAndroid + + private lateinit var application: Application + private lateinit var executorService: ExecutorService + @Before + fun setup(){ + MockKAnnotations.init() + application = ApplicationProvider.getApplicationContext() + val logger = TestLogger() + every{configuration.defaultProcessName} returns null + every{configuration.multiProcessEnabled} returns false + every{configuration.networkExecutor} returns mockk() + every{analytics.logger} returns logger + every{analytics.currentConfigurationAndroid} returns configuration + every{analytics.currentConfiguration} returns configuration + every { analytics.writeKey } returns "test_write_key" + every { analytics.shutdown() } just runs + executorService = Executors.newSingleThreadExecutor() + DummyAnalyticsFactory.analytics = analytics + } + + @Test + fun testRudderSyncWorker(){ + every{analytics.blockingFlush()} returns true + every { analytics.isShutdown } returns false + val worker = TestWorkerBuilder(application,executorService, + ).setInputData(analytics.workerInputData( + DummyAnalyticsFactory::class.java + )).build() + val result = worker.doWork() + assertThat(result, Matchers.`is`(ListenableWorker.Result.success())) + verify(exactly = 1){ + analytics.blockingFlush() + } + } + class DummyAnalyticsFactory: WorkManagerAnalyticsFactory { + companion object{ + lateinit var analytics: Analytics + } + override fun createAnalytics(application: Application): Analytics { + return analytics + } + + } +} diff --git a/libs/sync/src/test/java/com/rudderstack/android/sync/utils/TestLogger.kt b/libs/sync/src/test/java/com/rudderstack/android/sync/utils/TestLogger.kt new file mode 100644 index 000000000..bfe2aabda --- /dev/null +++ b/libs/sync/src/test/java/com/rudderstack/android/sync/utils/TestLogger.kt @@ -0,0 +1,38 @@ +/* + * Creator: Debanjan Chatterjee on 27/07/23, 7:28 pm Last modified: 27/07/23, 7:28 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sync.utils + +import com.rudderstack.core.Logger + +class TestLogger : Logger { + override fun activate(level: Logger.LogLevel) { + + } + + override fun info(tag: String, log: String) { + } + + override fun debug(tag: String, log: String) { + } + + override fun warn(tag: String, log: String) { + } + + override fun error(tag: String, log: String, throwable: Throwable?) { + } + + override val level: Logger.LogLevel + get() = Logger.LogLevel.DEBUG +} diff --git a/libs/test-common/.gitignore b/libs/test-common/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/libs/test-common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/test-common/build.gradle.kts b/libs/test-common/build.gradle.kts new file mode 100644 index 000000000..28cc599e6 --- /dev/null +++ b/libs/test-common/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Creator: Debanjan Chatterjee on 05/12/23, 12:04 pm Last modified: 05/12/23, 12:04 pm + * Copyright: All rights reserved Ⓒ 2023 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") +} + +java { + sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION +} +tasks.withType().all { + kotlinOptions { + jvmTarget = RudderstackBuildConfig.Build.JVM_TARGET + } +} +dependencies { + compileOnly(project(":core")) +} diff --git a/libs/test-common/src/main/java/com/vagabond/testcommon/MockConfigDownloadService.kt b/libs/test-common/src/main/java/com/vagabond/testcommon/MockConfigDownloadService.kt new file mode 100644 index 000000000..d98140c9f --- /dev/null +++ b/libs/test-common/src/main/java/com/vagabond/testcommon/MockConfigDownloadService.kt @@ -0,0 +1,33 @@ +package com.vagabond.testcommon + +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.models.RudderServerConfig + +class MockConfigDownloadService( + val mockConfigDownloadSuccess: Boolean = true, + val mockLastErrorMsg: String? = null, + val mockConfig: RudderServerConfig = RudderServerConfig( + source = RudderServerConfig.RudderServerConfigSource(), + ) +) : ConfigDownloadService { + + override lateinit var analytics: Analytics + + override fun download(callback: (success: Boolean, RudderServerConfig?, lastErrorMsg: String?) -> Unit) { + callback(mockConfigDownloadSuccess, mockConfig, mockLastErrorMsg) + } + + override fun addListener(listener: ConfigDownloadService.Listener, replay: Int) { + // Not-required + } + + override fun removeListener(listener: ConfigDownloadService.Listener) { + // Not-required + } + + override fun shutdown() { + // Not-required + } + +} diff --git a/libs/test-common/src/main/java/com/vagabond/testcommon/TestAnalyticsProvider.kt b/libs/test-common/src/main/java/com/vagabond/testcommon/TestAnalyticsProvider.kt new file mode 100644 index 000000000..6c1de4130 --- /dev/null +++ b/libs/test-common/src/main/java/com/vagabond/testcommon/TestAnalyticsProvider.kt @@ -0,0 +1,77 @@ +@file:JvmName("TestAnalyticsProvider") + +package com.vagabond.testcommon + +import com.rudderstack.core.Analytics +import com.rudderstack.core.ConfigDownloadService +import com.rudderstack.core.Configuration +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.Plugin +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message +import com.rudderstack.rudderjsonadapter.JsonAdapter + +private const val DUMMY_WRITE_KEY = "DUMMY_WRITE_KEY" +private var currentTestPlugin: Plugin? = null +private var inputs = listOf() +val inputVerifyPlugin = object : Plugin { + override lateinit var analytics: Analytics + override fun intercept(chain: Plugin.Chain): Message { + return chain.proceed(chain.message().also { + inputs += it.copy() + }) + } +} + +fun generateTestAnalytics(jsonAdapter: JsonAdapter): Analytics { + return generateTestAnalytics( + Configuration( + jsonAdapter = jsonAdapter, + shouldVerifySdk = false + ) + ) +} + +fun generateTestAnalytics( + mockConfiguration: Configuration, + configDownloadService: ConfigDownloadService = + MockConfigDownloadService(), + storage: Storage = VerificationStorage(), + dataUploadService: DataUploadService = TestDataUploadService(), +): Analytics { + val testingConfig = mockConfiguration + return Analytics( + DUMMY_WRITE_KEY, testingConfig, dataUploadService = dataUploadService, + configDownloadService = configDownloadService, storage = storage + ).also { + it.addPlugin(inputVerifyPlugin) + } +} + +fun Analytics.testPlugin(pluginUnderTest: Plugin) { + currentTestPlugin = pluginUnderTest + addPlugin(pluginUnderTest) +} + +fun Analytics.assertArguments(verification: Verification, List>) { + busyWait(100) + verification.assert( + inputs.toList(), storage.getDataSync() ?: emptyList() + ) +} + +fun Analytics.assertArgument(verification: Verification) { + busyWait(100) + verification.assert(inputs.lastOrNull(), storage.getDataSync().lastOrNull()) +} + +private fun busyWait(millis: Long) { + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < millis) { + // busy wait + } +} + +fun interface Verification { + fun assert(input: IN, output: OUT) +} diff --git a/libs/test-common/src/main/java/com/vagabond/testcommon/TestDataUploadService.kt b/libs/test-common/src/main/java/com/vagabond/testcommon/TestDataUploadService.kt new file mode 100644 index 000000000..b0d4f1e62 --- /dev/null +++ b/libs/test-common/src/main/java/com/vagabond/testcommon/TestDataUploadService.kt @@ -0,0 +1,38 @@ +package com.vagabond.testcommon + +import com.rudderstack.core.Analytics +import com.rudderstack.core.DataUploadService +import com.rudderstack.core.models.Message +import com.rudderstack.web.HttpResponse + +class TestDataUploadService : DataUploadService { + + override lateinit var analytics: Analytics + private var headers = mutableMapOf() + + var mockUploadStatus = 200 + var mockUploadBody = "OK" + var errorBody: String? = null + var error: Throwable? = null + override fun addHeaders(headers: Map) { + this.headers += headers + } + + override fun upload( + data: List, + extraInfo: Map?, + callback: (response: HttpResponse) -> Unit + ) { + callback(HttpResponse(mockUploadStatus, mockUploadBody, errorBody, error)) + } + + override fun uploadSync( + data: List, extraInfo: Map? + ): HttpResponse? { + return HttpResponse(mockUploadStatus, mockUploadBody, errorBody, error) + } + + override fun shutdown() { + headers = mutableMapOf() + } +} diff --git a/libs/test-common/src/main/java/com/vagabond/testcommon/VerificationStorage.kt b/libs/test-common/src/main/java/com/vagabond/testcommon/VerificationStorage.kt new file mode 100644 index 000000000..b22922b8e --- /dev/null +++ b/libs/test-common/src/main/java/com/vagabond/testcommon/VerificationStorage.kt @@ -0,0 +1,103 @@ +package com.vagabond.testcommon + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Storage +import com.rudderstack.core.models.Message +import com.rudderstack.core.models.RudderServerConfig + +class VerificationStorage : Storage { + + override lateinit var analytics: Analytics + private var storageQ = mutableListOf() + private var _serverConfig: RudderServerConfig? = null + override fun setStorageCapacity(storageCapacity: Int) { + /* No-op . */ + } + + override fun setMaxFetchLimit(limit: Int) { + /* No-op . */ + } + + override fun saveMessage(vararg messages: Message) { + + storageQ.addAll(messages) + } + + override fun setBackpressureStrategy(strategy: Storage.BackPressureStrategy) { + /* No-op . */ + } + + override fun deleteMessages(messages: List) { + // does not support async delete + deleteMessagesSync(messages) + } + + override fun addMessageDataListener(listener: Storage.DataListener) { + /* No-op . */ + } + + override fun removeMessageDataListener(listener: Storage.DataListener) { + /* No-op . */ + } + + override fun getData(offset: Int, callback: (List) -> Unit) { + callback.invoke(storageQ.toList()) + } + + override fun getCount(callback: (Long) -> Unit) { + callback.invoke(storageQ.size.toLong()) + } + + override fun getDataSync(offset: Int): List { + return storageQ.toList() + } + + override fun saveServerConfig(serverConfig: RudderServerConfig) { + /* No-op . */ + } + + override val serverConfig: RudderServerConfig? + get() = _serverConfig + + override fun saveOptOut(optOut: Boolean) { + /* No-op . */ + } + + override fun saveStartupMessageInQueue(message: Message) { + /* No-op . */ + } + + override fun clearStartupQueue() { + /* No-op . */ + } + + override fun shutdown() { + /* No-op . */ + } + + override fun clearStorage() { + storageQ.clear() + } + + override fun deleteMessagesSync(messages: List) { + storageQ -= messages.toSet() + } + + override val startupQueue: List + get() = /* No-op . */ listOf() + override val isOptedOut: Boolean + get() = false + override val optOutTime: Long + get() = 0L + override val optInTime: Long + get() = 0L + override val libraryName: String + get() = "Rudder-Test-Library" + override val libraryVersion: String + get() = 1.0.toString() + override val libraryPlatform: String + get() = "Android" + override val libraryOsVersion: String + get() = "13" + +} diff --git a/libs/test-common/src/main/java/com/vagabond/testcommon/utils/TestExecutor.kt b/libs/test-common/src/main/java/com/vagabond/testcommon/utils/TestExecutor.kt new file mode 100644 index 000000000..7980e0fcf --- /dev/null +++ b/libs/test-common/src/main/java/com/vagabond/testcommon/utils/TestExecutor.kt @@ -0,0 +1,34 @@ +package com.vagabond.testcommon.utils + +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.TimeUnit + +class TestExecutor : AbstractExecutorService() { + private var _isShutdown = false + override fun execute(command: Runnable) { + command.run() + } + + override fun shutdown() { + //No op + _isShutdown = true + } + + override fun shutdownNow(): MutableList { + // No op + shutdown() + return mutableListOf() + } + + override fun isShutdown(): Boolean { + return _isShutdown + } + + override fun isTerminated(): Boolean { + return _isShutdown + } + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { + return false + } +} diff --git a/models/CHANGELOG.md b/models/CHANGELOG.md deleted file mode 100644 index 9a33c20b1..000000000 --- a/models/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog - -This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). - -## 0.1.0 (2023-09-19) - - -### Features - -* metrics v2 ([#256](https://github.com/rudderlabs/rudder-sdk-android/issues/256)) ([c0b9639](https://github.com/rudderlabs/rudder-sdk-android/commit/c0b96397a14c5ff5baa3900804fd3b5b02d21304)) - -## 0.1.0 (2023-09-19) - - -### Features - -* metrics v2 ([#256](https://github.com/rudderlabs/rudder-sdk-android/issues/256)) ([c0b9639](https://github.com/rudderlabs/rudder-sdk-android/commit/c0b96397a14c5ff5baa3900804fd3b5b02d21304)) - -## 0.1.0 (2023-08-01) - - -### Features - -* metrics v2 ([#256](https://github.com/rudderlabs/rudder-sdk-android/issues/256)) ([c0b9639](https://github.com/rudderlabs/rudder-sdk-android/commit/c0b96397a14c5ff5baa3900804fd3b5b02d21304)) diff --git a/models/CHANGELOG_LATEST.md b/models/CHANGELOG_LATEST.md deleted file mode 100644 index 0dfe60f70..000000000 --- a/models/CHANGELOG_LATEST.md +++ /dev/null @@ -1,7 +0,0 @@ -## 0.1.0 (2023-09-19) - - -### Features - -* metrics v2 ([#256](https://github.com/rudderlabs/rudder-sdk-android/issues/256)) ([c0b9639](https://github.com/rudderlabs/rudder-sdk-android/commit/c0b96397a14c5ff5baa3900804fd3b5b02d21304)) - diff --git a/models/build.gradle b/models/build.gradle deleted file mode 100644 index f0dceefe1..000000000 --- a/models/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -compileKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} - -apply from : "$rootDir/dependencies.gradle" - -dependencies { - - //json parsers as compileOnly just for annotations - compileOnly deps.gson - compileOnly deps.jackson - testImplementation 'junit:junit:4.+' -} -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') diff --git a/models/gradle.properties b/models/gradle.properties deleted file mode 100644 index c132336af..000000000 --- a/models/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ - -POM_ARTIFACT_ID=models -GROUP=com.rudderstack.kotlin.sdk -POM_PACKAGING=jar -VERSION_CODE=2 -VERSION_NAME=0.1.0 -POM_NAME=Rudderstack Kotlin SDK Models -POM_DESCRIPTION=Rudderstack Kotlin SDK Models \ No newline at end of file diff --git a/models/src/main/java/com/rudderstack/models/CustomerEntity.kt b/models/src/main/java/com/rudderstack/models/CustomerEntity.kt deleted file mode 100644 index f5ed9c001..000000000 --- a/models/src/main/java/com/rudderstack/models/CustomerEntity.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.rudderstack.android.models - -import com.fasterxml.jackson.annotation.JsonProperty -import com.google.gson.annotations.SerializedName - -data class CustomerEntity( - @SerializedName("name") - @get:JsonProperty - val name: String, - @SerializedName("address") - @get:JsonProperty - val address: String, - @SerializedName("work_address") - @get:JsonProperty("work_address") - val workAddress: String, -) diff --git a/models/src/main/java/com/rudderstack/models/OrderEntity.kt b/models/src/main/java/com/rudderstack/models/OrderEntity.kt deleted file mode 100644 index af60ed93a..000000000 --- a/models/src/main/java/com/rudderstack/models/OrderEntity.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.rudderstack.android.models - -import com.fasterxml.jackson.annotation.JsonProperty -import com.google.gson.annotations.SerializedName - -data class OrderEntity( - @SerializedName("order_id") - @get:JsonProperty("order_id") - val orderId: Int, - @SerializedName("quantity") - @get:JsonProperty("quantity") - var quantity: Int, - @SerializedName("total_price") - @get:JsonProperty("total_price") - val totalPrice: Double, -) diff --git a/moshirudderadapter/build.gradle b/moshirudderadapter/build.gradle deleted file mode 100644 index 53d8893ee..000000000 --- a/moshirudderadapter/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -compileKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} - -apply from : "$projectDir/../dependencies.gradle" - - -dependencies { - - implementation(project(path: ":rudderjsonadapter", configuration: 'default')) - api deps.moshi.kotlin - api deps.moshi.core - //test - testImplementation deps.hamcrest - testImplementation 'junit:junit:4.+' -} -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/moshirudderadapter/build.gradle.kts b/moshirudderadapter/build.gradle.kts new file mode 100644 index 000000000..ca059b871 --- /dev/null +++ b/moshirudderadapter/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("java-library") + id("kotlin") +} + +dependencies { + implementation(project(":rudderjsonadapter")) + api(libs.moshi) + api(libs.moshi.kotlin) + testImplementation(libs.junit) + testImplementation(libs.hamcrest) + +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-jar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/moshirudderadapter/src/main/java/com/rudderstack/moshirudderadapter/MoshiAdapter.kt b/moshirudderadapter/src/main/java/com/rudderstack/moshirudderadapter/MoshiAdapter.kt index 6b24dfd1e..49f690816 100644 --- a/moshirudderadapter/src/main/java/com/rudderstack/moshirudderadapter/MoshiAdapter.kt +++ b/moshirudderadapter/src/main/java/com/rudderstack/moshirudderadapter/MoshiAdapter.kt @@ -25,9 +25,11 @@ import java.lang.reflect.Type * * A Moshi based implementation of JsonAdapter */ -class MoshiAdapter(private var moshi : Moshi = Moshi.Builder() - .addLast(KotlinJsonAdapterFactory()) - .build()) : JsonAdapter { +class MoshiAdapter( + private var moshi: Moshi = Moshi.Builder() + .addLast(KotlinJsonAdapterFactory()) + .build(), +) : JsonAdapter { override fun readJson(json: String, typeAdapter: RudderTypeAdapter): T? { val jsonAdapter = typeAdapter.type?.let { @@ -55,7 +57,7 @@ class MoshiAdapter(private var moshi : Moshi = Moshi.Builder() } override fun readJson(json: String, resultClass: Class): T? { - //in case T is primitive, json needs to be returned as primitive + // in case T is primitive, json needs to be returned as primitive return when (resultClass) { String::class.java, CharSequence::class.java -> json as T @@ -68,12 +70,10 @@ class MoshiAdapter(private var moshi : Moshi = Moshi.Builder() moshi.adapter(resultClass) as com.squareup.moshi.JsonAdapter return adapter.fromJson(json) } - } - } - fun add(factory: com.squareup.moshi.JsonAdapter.Factory){ - moshi = moshi.newBuilder().add(factory).build() + fun add(factory: com.squareup.moshi.JsonAdapter.Factory) { + moshi = moshi.newBuilder().add(factory).build() } fun add(type: Type, jsonAdapter: com.squareup.moshi.JsonAdapter) { moshi = moshi.newBuilder().add(type, jsonAdapter).build() @@ -85,8 +85,8 @@ class MoshiAdapter(private var moshi : Moshi = Moshi.Builder() fun add( type: Type, annotation: Class, - jsonAdapter: com.squareup.moshi.JsonAdapter - ){ + jsonAdapter: com.squareup.moshi.JsonAdapter, + ) { moshi = moshi.newBuilder().add(type, annotation, jsonAdapter).build() } diff --git a/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ParsingTest.kt b/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ParsingTest.kt index 89a3377d5..31424ac65 100644 --- a/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ParsingTest.kt +++ b/moshirudderadapter/src/test/java/com/rudderstack/moshirudderadapter/ParsingTest.kt @@ -23,52 +23,56 @@ class ParsingTest { data class SomeClass(val name: String, val prop: String) val someJson = "{" + - "\"type1\" : [" + - "{" + - "\"name\":\"ludo\"," + - "\"prop\":\"iok\"" + - "}" + - "]" + - "}" - //for checking map conversion - data class MapClass(val name: String, val age : Int) + "\"type1\" : [" + + "{" + + "\"name\":\"ludo\"," + + "\"prop\":\"iok\"" + + "}" + + "]" + + "}" + // for checking map conversion + data class MapClass(val name: String, val age: Int) @Test fun checkDeserialization() { // val type = Map::class.java.typeName val rta = object : RudderTypeAdapter>>() {} val ja = MoshiAdapter() - val res = ja.readJson>>( someJson, rta) + val res = ja.readJson>>(someJson, rta) assert(res != null) assert(res!!["type1"] != null) - assert(res["type1"]?.size?:0 ==1) + assert(res["type1"]?.size ?: 0 == 1) assert(res["type1"]?.get(0)?.name == "ludo") assert(res["type1"]?.get(0)?.prop == "iok") - } + @Test - fun checkSerialization(){ + fun checkSerialization() { val someClass = SomeClass("ludo", "iok") val ja = MoshiAdapter() - val res = ja.writeToJson>>(mapOf(Pair("type1", listOf(someClass)) ), - object : RudderTypeAdapter>>(){}) - assert(res == someJson.replace(" ","")) + val res = ja.writeToJson>>( + mapOf(Pair("type1", listOf(someClass))), + object : RudderTypeAdapter>>() {}, + ) + assert(res == someJson.replace(" ", "")) } @Test - fun checkMapToObjConversion(){ + fun checkMapToObjConversion() { val mapRepresentation = mapOf("name" to "Foo", "age" to 20) val adapter = MoshiAdapter() - val outCome : MapClass? = adapter.readMap(mapRepresentation, MapClass::class.java) + val outCome: MapClass? = adapter.readMap(mapRepresentation, MapClass::class.java) - MatcherAssert.assertThat(outCome, Matchers.allOf( - Matchers.notNullValue(), - Matchers.isA(MapClass::class.java), - Matchers.hasProperty("name", Matchers.equalTo("Foo")), - Matchers.hasProperty("age", Matchers.equalTo(20)) - )) + MatcherAssert.assertThat( + outCome, + Matchers.allOf( + Matchers.notNullValue(), + Matchers.isA(MapClass::class.java), + Matchers.hasProperty("name", Matchers.equalTo("Foo")), + Matchers.hasProperty("age", Matchers.equalTo(20)), + ), + ) } - -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 0924a937c..262761235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@commitlint/cli": "17.7.2", "@commitlint/config-conventional": "17.7.0", "@digitalroute/cz-conventional-changelog-for-jira": "8.0.1", - "@jnxplus/nx-gradle": "0.6.1", + "@jnxplus/nx-gradle": "0.40.1-2", "@jscutlery/semver": "3.1.0", "@nx/workspace": "16.10.0", "commitizen": "4.3.0", @@ -572,45 +572,372 @@ } }, "node_modules/@jnxplus/common": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@jnxplus/common/-/common-0.9.1.tgz", - "integrity": "sha512-e1Ev0vbWCPd4J8hSfAYD260QwqRDx4M8FJcYBTzzHg0RjH8JoEE9UecGJshNrvY/ZifgLeyVEiUU3xuC5GNpgQ==", + "version": "0.40.1-2", + "resolved": "https://registry.npmjs.org/@jnxplus/common/-/common-0.40.1-2.tgz", + "integrity": "sha512-szbw+3TNXxAT62Fw0V7+NS0tE43gzOcV6QJzn9Jahm0pqz6QT0t7D9NT2q98BUY9uOVW20UE55qBCFx7lYcIgQ==", "dev": true, "dependencies": { - "tslib": "2.5.0" + "@nx/devkit": ">=17.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@jnxplus/common/node_modules/@nrwl/devkit": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-18.0.8.tgz", + "integrity": "sha512-cKtXq2I/3y/t1I+jMn8XVfhtSjGxJHKGSmxStMdRPMcUim8iaS2V3fDUdF2CGrXrtbmDtYwBC8413YY+nVh0Gw==", + "dev": true, + "dependencies": { + "@nx/devkit": "18.0.8" + } + }, + "node_modules/@jnxplus/common/node_modules/@nx/devkit": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-18.0.8.tgz", + "integrity": "sha512-df56bzmhF8yhVCCChe0ATjCsc9r9SNcpks5/bABGqR91vHVGfsz0H33RYO7P2jrPwFBRYnf+aWWFY1d6NpEdxw==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "18.0.8", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" }, "peerDependencies": { - "@nx/devkit": ">=16.0.0" + "nx": ">= 16 <= 18" + } + }, + "node_modules/@jnxplus/nx-gradle": { + "version": "0.40.1-2", + "resolved": "https://registry.npmjs.org/@jnxplus/nx-gradle/-/nx-gradle-0.40.1-2.tgz", + "integrity": "sha512-ZtAMukkeZeygXSgRbyXN0YptU8Zv0cTs08GFTiRX2dC8JOBr7SaMttGApCnDdwxvKNsDR7LVOmyVLIqpkNDzAA==", + "dev": true, + "dependencies": { + "@jnxplus/common": "0.40.1-2", + "@nx/devkit": ">=17.0.0", + "nx": ">=17.0.0", + "smol-toml": "^1.1.3", + "tslib": "^2.6.2" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nrwl/devkit": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-18.0.8.tgz", + "integrity": "sha512-cKtXq2I/3y/t1I+jMn8XVfhtSjGxJHKGSmxStMdRPMcUim8iaS2V3fDUdF2CGrXrtbmDtYwBC8413YY+nVh0Gw==", + "dev": true, + "dependencies": { + "@nx/devkit": "18.0.8" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nrwl/tao": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-18.0.8.tgz", + "integrity": "sha512-zBzdv9mGBaWtBbujbLCVzG7ZI5npUg9fnUz8VtjN6jydAQEL/Uqj5mPlFYQPPBAw2xwF8TL9ZX/rOoAWHnJtjw==", + "dev": true, + "dependencies": { + "nx": "18.0.8", + "tslib": "^2.3.0" + }, + "bin": { + "tao": "index.js" } }, - "node_modules/@jnxplus/gradle": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@jnxplus/gradle/-/gradle-0.13.2.tgz", - "integrity": "sha512-Lq4GrzKPy2RZOq44yUSOTWpCKuL7pZ1QUFB1qy6Dv3y3tW3tzQ1U6v73DT6tCKrdWzSCkMVOzuqcE1T7EKyOkw==", + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/devkit": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-18.0.8.tgz", + "integrity": "sha512-df56bzmhF8yhVCCChe0ATjCsc9r9SNcpks5/bABGqR91vHVGfsz0H33RYO7P2jrPwFBRYnf+aWWFY1d6NpEdxw==", "dev": true, "dependencies": { - "@jnxplus/common": "0.9.1", - "tslib": "2.5.0" + "@nrwl/devkit": "18.0.8", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" }, "peerDependencies": { - "@nx/devkit": ">=16.0.0" + "nx": ">= 16 <= 18" } }, - "node_modules/@jnxplus/nx-gradle": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@jnxplus/nx-gradle/-/nx-gradle-0.6.1.tgz", - "integrity": "sha512-L4aj/QAUizu3iLhidzeB5KZWrsQSeEx2ss8m5gg5/GvlI1ubEUEobNiHT5GlS8jkxcU9TRdhS+d6xI1iwf8EgA==", + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-darwin-arm64": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.0.8.tgz", + "integrity": "sha512-B2vX90j1Ex9Mki/Fai45UJ0r7mPc/xLBzQYQ9MFI2XoUXKhYl5BVBfJ+EbJ2PBcIXAnp44qY0wyxEpp+8Glxcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-darwin-x64": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-18.0.8.tgz", + "integrity": "sha512-nC172j4LwOqc22BtJGsrjPYGhZ6EFXhYi0ceb6yzEA1Z32Wl98OXbAcbbhyEcuL3iYI9VrZgzAAzIUo7l4injw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-freebsd-x64": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-18.0.8.tgz", + "integrity": "sha512-Qoz668WMB6nxdMFG5X88B7W72+d5K/95XEFKY2022EPm88DQFFcJAfdkMrRkeO3yBJtwLAAK+Jyni9uAfOXzGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-18.0.8.tgz", + "integrity": "sha512-0RTuJTaAmE7Xjc0P0DIbbYEhPGBILCii2nOz6vwTEzIqxSMgXW4T1g1zSDKCiUUyS6HVffGvCTNvuHuoYY2DMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-linux-arm64-gnu": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-18.0.8.tgz", + "integrity": "sha512-fmwsrDeeY44f6cCnfrXNuvFEzqvD/A5yg3TVwZoKldWRAG5gexj4AWpBHqgGTcCj6ky1NGxnlaktKC5geGhJhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-linux-arm64-musl": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-18.0.8.tgz", + "integrity": "sha512-jz1dzQlrfZteJdsEJ1MbjI7m2jkBLhLe5y9x+96/KgmJbCV7LD9RLevWIzz7FDuhfJziMOoSrGdaW47G13p/Fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-linux-x64-gnu": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.0.8.tgz", + "integrity": "sha512-eq2AAZN4fsjhABtU76eroFHcNK6QWo4eMAH7tcZUoGLwfBAo+wPYggxm9LNZ5weKVxwqySHavlXd5rNA26WrbA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-linux-x64-musl": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-18.0.8.tgz", + "integrity": "sha512-FBHVJ0DtBqQynbQImg1kc9/WfRGSvbRNzaqI2rO/zO0j2BeT9BQ8byTn2EiMBxz72LSbqEmtQtqe5w50hAsKcA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-win32-arm64-msvc": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-18.0.8.tgz", + "integrity": "sha512-qphQIIfwAR03s7ifPVc0XhjdOeep2hOoZN2jN5ShG1QD/DIipNnMrRK21M6JcoP7soRPpkJFlI5Yaleh9/EJhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/@nx/nx-win32-x64-msvc": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.0.8.tgz", + "integrity": "sha512-XP8hle+cPNH5n18iTM7l0q07zEdvoPcHYVr5IoYOA54Ke9ZUxau4owUeok2HhLr61o2u0CTwf1vWoV+Y1AUAdg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/nx": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/nx/-/nx-18.0.8.tgz", + "integrity": "sha512-IhzRLCZaiR9zKGJ3Jm79bhi8nOdyRORQkFc/YDO6xubLSQ5mLPAeg789Q/SlGRzU5oMwLhm5D/gvvMJCAvUmXQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@jnxplus/common": "0.9.1", - "@jnxplus/gradle": "0.13.2", - "prettier": "^2.8.7", - "prettier-plugin-java": "^2.1.0", - "tslib": "2.5.0" + "@nrwl/tao": "18.0.8", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.0-rc.46", + "@zkochan/js-yaml": "0.0.6", + "axios": "^1.6.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.3.1", + "dotenv-expand": "~10.0.0", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^11.1.0", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "js-yaml": "4.1.0", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "18.0.8", + "@nx/nx-darwin-x64": "18.0.8", + "@nx/nx-freebsd-x64": "18.0.8", + "@nx/nx-linux-arm-gnueabihf": "18.0.8", + "@nx/nx-linux-arm64-gnu": "18.0.8", + "@nx/nx-linux-arm64-musl": "18.0.8", + "@nx/nx-linux-x64-gnu": "18.0.8", + "@nx/nx-linux-x64-musl": "18.0.8", + "@nx/nx-win32-arm64-msvc": "18.0.8", + "@nx/nx-win32-x64-msvc": "18.0.8" }, "peerDependencies": { - "@nx/devkit": ">=16.3.0" + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@jnxplus/nx-gradle/node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@jridgewell/resolve-uri": { @@ -706,12 +1033,6 @@ "tao": "index.js" } }, - "node_modules/@nrwl/tao/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/@nrwl/workspace": { "version": "16.10.0", "resolved": "https://registry.npmjs.org/@nrwl/workspace/-/workspace-16.10.0.tgz", @@ -754,12 +1075,6 @@ "node": ">=10" } }, - "node_modules/@nx/devkit/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/@nx/nx-darwin-arm64": { "version": "16.10.0", "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.10.0.tgz", @@ -933,12 +1248,6 @@ "yargs-parser": "21.1.1" } }, - "node_modules/@nx/workspace/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -1082,12 +1391,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@yarnpkg/parsers/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/@zkochan/js-yaml": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", @@ -1261,12 +1564,12 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -1460,15 +1763,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/chevrotain": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", - "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", - "dev": true, - "dependencies": { - "regexp-to-ast": "0.4.0" - } - }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -2525,9 +2819,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -3217,16 +3511,6 @@ "node": ">=10" } }, - "node_modules/java-parser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.0.5.tgz", - "integrity": "sha512-AwieTO24Itcu0GgP9pBXs8gkqBtkmReclpBgXF4NkbIjdS7cn7hqpebjTmb5ouYYLFR+m3yh5fR3nW1NRrthdg==", - "dev": true, - "dependencies": { - "chevrotain": "6.5.0", - "lodash": "4.17.21" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -4032,12 +4316,6 @@ "node": ">=10" } }, - "node_modules/nx/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4258,47 +4536,6 @@ "node": ">=0.10.0" } }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-java": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.3.1.tgz", - "integrity": "sha512-OYn8skqKnE5YUVL8f2ocayA6XCJK8PqsEz3pfATbDqzgdaSYDLhE/s8KrXrX9gj8KXIG6Wx0CMoXTNH8+ED22w==", - "dev": true, - "dependencies": { - "java-parser": "2.0.5", - "lodash": "4.17.21", - "prettier": "3.0.3" - } - }, - "node_modules/prettier-plugin-java/node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4545,12 +4782,6 @@ "node": ">=8" } }, - "node_modules/regexp-to-ast": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", - "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", - "dev": true - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4675,12 +4906,6 @@ "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4749,6 +4974,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/smol-toml": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.1.4.tgz", + "integrity": "sha512-Y0OT8HezWsTNeEOSVxDnKOW/AyNXHQ4BwJNbAXlLTF5wWsBvrcHhIkE5Rf8kQMLmgf7nDX3PVOlgC6/Aiggu3Q==", + "dev": true, + "engines": { + "node": ">= 18", + "pnpm": ">= 8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5106,9 +5341,9 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/type-fest": { diff --git a/package.json b/package.json index ff0dbe11f..6c5afc91a 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "bump-version:monorepo": "npm version minor --git-tag-version false", "build": "./gradlew build", "lint": "nx affected --base=origin/master-v2 --target=lint --parallel=4 --exclude=app,core,android --skipCommitTypes=docs,ci,chore", - "ktformat": "nx affected --base=origin/master-v2 --target=ktformat --parallel=1 --exclude=app,core,android --skipCommitTypes=docs,ci,chore", - "sync-version-gradle": "nx affected --target=sync-bumped-version-properties --parallel=1 --exclude=app,core,android --skipCommitTypes=docs,ci,chore", - "test": "nx affected --base=origin/master-v2 --target='test' --parallel=1 --exclude=app,core,android --skipCommitTypes=docs,ci,chore", - "release": "nx affected --base=origin/master-v2 --target=version --parallel=1 --exclude=app,core,android --skipCommitTypes=docs,ci,chore && npm run sync-version-gradle", - "release:github": "nx affected --target=github --parallel=1 --exclude=app,core,android --skipCommitTypes=docs,ci,chore", - "release:sonatype": "nx affected --base=origin/master-v2 --target=release-sonatype --parallel=1 --exclude=app,core,android", - "release-snapshot:sonatype": "nx affected --base=origin/master-v2 --target=snapshot-release --parallel=1 --exclude=app,core,android" + "ktformat": "nx affected --base=origin/master-v2 --target=ktformat --parallel=1 --exclude=app --skipCommitTypes=docs,ci", + "sync-version-gradle": "nx affected --target=sync-bumped-version-properties --parallel=1 --exclude=app --skipCommitTypes=docs,ci,chore", + "test": "nx affected --base=origin/master-v2 --target='test' --parallel=1 --exclude=app --skipCommitTypes=docs,ci,chore", + "release": "nx affected --base=origin/master-v2 --target=version --parallel=1 --exclude=app --skipCommitTypes=docs,ci,chore && npm run sync-version-gradle", + "release:github": "nx affected --target=github --parallel=1 --exclude=app --skipCommitTypes=docs,ci,chore", + "release:sonatype": "nx affected --base=origin/master-v2 --target=release-sonatype --parallel=1 --exclude=app", + "release-snapshot:sonatype": "nx affected --base=origin/master-v2 --target=snapshot-release --parallel=1 --exclude=app" }, "private": true, "devDependencies": { @@ -37,7 +37,7 @@ "@jscutlery/semver": "3.1.0", "commitizen": "4.3.0", "commitlint": "17.7.2", - "@jnxplus/nx-gradle": "0.6.1", + "@jnxplus/nx-gradle": "0.40.1-2", "@nx/workspace": "16.10.0", "nx": "16.10.0" }, diff --git a/repository/build.gradle b/repository/build.gradle deleted file mode 100644 index dd409dae2..000000000 --- a/repository/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'com.android.library' - id 'kotlin-android' -} -apply from : "$projectDir/../dependencies.gradle" - -android { - namespace 'com.rudderstack.android.repository' - compileSdk library.target_sdk - - defaultConfig { - minSdk library.min_sdk - targetSdk library.target_sdk - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - kotlinOptions { - jvmTarget = '1.8' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - - testImplementation deps.androidXTestExtJunitKtx - testImplementation deps.androidXTestRules - - testImplementation deps.junit - testImplementation deps.androidXTest - testImplementation deps.hamcrest - testImplementation deps.mockito - testImplementation deps.awaitility - testImplementation deps.robolectric -} -apply from: rootProject.file('gradle/artifacts-aar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/repository/build.gradle.kts b/repository/build.gradle.kts new file mode 100644 index 000000000..e249cea78 --- /dev/null +++ b/repository/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + + namespace = "com.rudderstack.android.repository" + compileSdk = RudderstackBuildConfig.Android.TARGET_SDK + + defaultConfig { + minSdk = RudderstackBuildConfig.Android.MIN_SDK + targetSdk = RudderstackBuildConfig.Android.TARGET_SDK + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + named("release") { + isMinifyEnabled = false + setProguardFiles( + listOf( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + ) + } + } + compileOptions { + sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + } + kotlinOptions { + jvmTarget = RudderstackBuildConfig.Build.JVM_TARGET + javaParameters = true + } + testOptions { + unitTests { + this.isIncludeAndroidResources = true + } + } +} + +dependencies { + + testImplementation(libs.android.x.test.ext.junitktx) + testImplementation(libs.android.x.testrules) + testImplementation(libs.awaitility) + testImplementation(libs.hamcrest) + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.robolectric) + + androidTestImplementation(libs.android.x.test.ext.junitktx) + androidTestImplementation(libs.android.x.test.espresso) + androidTestImplementation(libs.android.x.testrules) +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-aar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/repository/src/androidTest/java/com/rudderstack/android/repository/ContentProviderTest.kt b/repository/src/androidTest/java/com/rudderstack/android/repository/ContentProviderTest.kt index 69934baef..2ce604825 100644 --- a/repository/src/androidTest/java/com/rudderstack/android/repository/ContentProviderTest.kt +++ b/repository/src/androidTest/java/com/rudderstack/android/repository/ContentProviderTest.kt @@ -14,9 +14,9 @@ package com.rudderstack.android.repository - import android.content.ContentValues -import android.database.Cursor +import android.content.UriMatcher +import android.net.Uri import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.provider.ProviderTestRule @@ -31,19 +31,24 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class ContentProviderTest { - companion object{ +class ContentProviderTest { + companion object { private const val DB_NAME = "test_db" private const val TABLE_NAME = "test_table" + private const val FIELD_NAME_1 = "model_name" + private const val FIELD_NAME_2 = "model_values" // private const val DB_INSERT_COMMAND_1 = "INSERT INTO $TABLE_NAME ('COL1', 'COL2') VALUES ('col1_val', 'col') " } + + private lateinit var database: RudderDatabase + @get:Rule - var mProviderRule: ProviderTestRule = - ProviderTestRule.Builder(EntityContentProvider::class.java, - "com.rudderstack.android.repository.test.EntityContentProvider" - ) - .build() - //lets have a model class + var mProviderRule: ProviderTestRule = ProviderTestRule.Builder( + EntityContentProvider::class.java, + "com.rudderstack.android.repository.test.EntityContentProvider", + ).setDatabaseCommands(DB_NAME).build() + + // lets have a model class data class Model(val name: String, val values: Array) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -63,32 +68,32 @@ class ContentProviderTest { return result } } - //let's create an entity for the same + // let's create an entity for the same @RudderEntity( - TABLE_NAME, fields = [ - RudderField(RudderField.Type.TEXT, "model_name", primaryKey = true), - RudderField(RudderField.Type.TEXT, "model_values"), + TABLE_NAME, + fields = [ + RudderField(RudderField.Type.TEXT, FIELD_NAME_1, primaryKey = true), + RudderField(RudderField.Type.TEXT, FIELD_NAME_2), - ] + ], ) class ModelEntity(val model: Model) : Entity { companion object { fun create(values: Map): ModelEntity { return ModelEntity( Model( - values["model_name"] as String, - (values["model_values"] as String).split(',').toTypedArray() - ) + values[FIELD_NAME_1] as String, + (values[FIELD_NAME_2] as String).split(',').toTypedArray(), + ), ) } } override fun generateContentValues(): ContentValues { - return ContentValues( - ).also { - it.put("model_name", model.name) - it.put("model_values", model.values.reduce { acc, s -> "$acc,$s" }) + return ContentValues().also { + it.put(FIELD_NAME_1, model.name) + it.put(FIELD_NAME_2, model.values.reduce { acc, s -> "$acc,$s" }) } } @@ -105,74 +110,125 @@ class ContentProviderTest { } } - //entity factory + // entity factory class ModelEntityFactory : EntityFactory { override fun getEntity(entity: Class, values: Map): T? { return when (entity) { ModelEntity::class.java -> ModelEntity.create(values) else -> null } as T? - } - } - private val testUri by lazy { - EntityContentProvider.getContentUri(TABLE_NAME, ApplicationProvider.getApplicationContext()) - } + private val testUri + get() = EntityContentProvider.getContentUri( + TABLE_NAME, + ApplicationProvider.getApplicationContext() + ) + + @Before fun initialize() { - RudderDatabase.init( + database = RudderDatabase( ApplicationProvider.getApplicationContext(), // RuntimeEnvironment.application, - DB_NAME, ModelEntityFactory() + DB_NAME, ModelEntityFactory(), true + ) + //create table for ModelEntity + EntityContentProvider.registerTableCommands( + DB_NAME, + "CREATE TABLE IF NOT EXISTS $TABLE_NAME ($FIELD_NAME_1 TEXT PRIMARY KEY, $FIELD_NAME_2 TEXT)", + null + ) } @After fun tearDown() { - RudderDatabase.shutDown() + database.shutDown() } + @Test - fun testContentProvider(){ - //let's start with insertion + fun testUriMatcherSuccess() { + val uriMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH) + val authority = "com.rudderstack.android.repository.test.EntityContentProvider" + val tableName = "test_table" +// val contentUri = Uri.parse("content://$authority/$tableName") + uriMatcher.addURI(authority, tableName, 1) + uriMatcher.addURI(authority, "$tableName/*", 2) + val testUri = Uri.parse( + "content://com.rudderstack.android.repository.test.EntityContentProvider" + "/test_table?db_entity=com.rudderstack.android.repository.ContentProviderTest%24ModelEntity&db_name=test_db" + ) + assertThat(uriMatcher.match(testUri), org.hamcrest.Matchers.`is`(1)) + + } + + @Test + fun testContentProviderWithTwoEntities() { + // let's start with insertion val modelEntity1 = ModelEntity(Model("event-1", arrayOf("1", "2"))) - val modelEntity2 = ModelEntity( Model("event-2", arrayOf("2", "3"))) + val modelEntity2 = ModelEntity(Model("event-2", arrayOf("2", "3"))) - val contentResolver = mProviderRule.resolver val modelTestUriBuilder = testUri.buildUpon().appendQueryParameter( - EntityContentProvider.ECP_ENTITY_CODE, ModelEntity::class.java.name + EntityContentProvider.ECP_ENTITY_CODE, + ModelEntity::class.java.name, + ).appendQueryParameter( + EntityContentProvider.ECP_DATABASE_CODE, DB_NAME + ) + val contentResolver = mProviderRule.resolver + + println("modelTestUriBuilder: $modelTestUriBuilder") + // insert + val uri1 = contentResolver.insert( + modelTestUriBuilder.build(), + modelEntity1.generateContentValues() + ) + println("uri1: $uri1") + assertThat( + uri1, + allOf( + notNullValue(), + ), ) - //insert - val uri1 = contentResolver.insert(modelTestUriBuilder.build(), modelEntity1.generateContentValues()) - assertThat(uri1, allOf( - notNullValue(), - )) val uri2 = contentResolver.insert(modelTestUriBuilder.build(), modelEntity2.generateContentValues()) - assertThat(uri2, allOf( - notNullValue(), - )) - //Two elements present - val cursor = contentResolver.query(modelTestUriBuilder.build(), null, null, null,null ) - assertThat(cursor?.count, allOf( - notNullValue(), - `is`(2) - )) + assertThat( + uri2, + allOf( + notNullValue(), + ), + ) +// // Two elements present + val cursor = contentResolver.query(modelTestUriBuilder.build(), null, null, null, null) + assertThat( + cursor?.count, + allOf( + notNullValue(), + `is`(2), + ), + ) cursor?.close() // fetch with limit 1 - val cursor2 = contentResolver.query(modelTestUriBuilder - .appendQueryParameter(EntityContentProvider.ECP_LIMIT_CODE, "1") - .build(), null, null, null,null ) - assertThat(cursor2?.count, allOf( - notNullValue(), - `is`(1) - )) + val cursor2 = contentResolver.query( + modelTestUriBuilder + .appendQueryParameter(EntityContentProvider.ECP_LIMIT_CODE, "1") + .build(), + null, + null, + null, + null, + ) + assertThat( + cursor2?.count, + allOf( + notNullValue(), + `is`(1), + ), + ) cursor2?.close() - //delete both + // delete both val delCount = contentResolver.delete(modelTestUriBuilder.build(), null, null) - //del count should be 2 + // del count should be 2 assertThat(delCount, `is`(2)) } - -} \ No newline at end of file +} diff --git a/repository/src/androidTest/java/com/rudderstack/android/repository/ExampleInstrumentedTest.kt b/repository/src/androidTest/java/com/rudderstack/android/repository/ExampleInstrumentedTest.kt deleted file mode 100644 index 38dd7ef33..000000000 --- a/repository/src/androidTest/java/com/rudderstack/android/repository/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.rudderstack.android.repository - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rudderstack.android.repository.test", appContext.packageName) - } -} diff --git a/repository/src/main/java/com/rudderstack/android/repository/Dao.kt b/repository/src/main/java/com/rudderstack/android/repository/Dao.kt index fa5ada768..544add660 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/Dao.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/Dao.kt @@ -42,26 +42,26 @@ class Dao( private val useContentProvider: Boolean, private val context: Context, private val entityFactory: EntityFactory, - private val executorService: ExecutorService + private val executorService: ExecutorService, + private val databaseName: String ) { companion object { - private val DB_LOCK = Any(); + private val DB_LOCK = Any() } private val todoLock = ReentrantLock(true) private val insertionLock = ReentrantLock(true) - private //create fields statement + private // create fields statement val fields = entityClass.getAnnotation(RudderEntity::class.java)?.fields?.takeIf { it.isNotEmpty() } - ?: throw IllegalArgumentException("There should be at least one field in @Entity") + ?: throw IllegalArgumentException("There should be at least one field in @Entity") private val tableName: String = entityClass.getAnnotation(RudderEntity::class.java)?.tableName - ?: throw IllegalArgumentException( - "${entityClass.simpleName} is being used to generate Dao, " + - "but missing @RudderEntity annotation" - ) + ?: throw IllegalArgumentException( + "${entityClass.simpleName} is being used to generate Dao, " + "but missing @RudderEntity annotation", + ) private var _db: SQLiteDatabase? = null get() = if (field?.isOpen == true) field else null @@ -70,7 +70,16 @@ class Dao( private val entityContentProviderUri by lazy { EntityContentProvider.getContentUri(tableName, context).buildUpon().appendQueryParameter( - EntityContentProvider.ECP_ENTITY_CODE, entityClass.name + EntityContentProvider.ECP_ENTITY_CODE, + entityClass.name, + ).appendQueryParameter(EntityContentProvider.ECP_DATABASE_CODE, databaseName) + } + + init { + if (useContentProvider) EntityContentProvider.registerTableCommands( + databaseName, createTableStmt( + tableName, fields + ), createIndexStmt(tableName, fields) ) } @@ -92,7 +101,6 @@ class Dao( val (rowIds, _) = insertData(db, this, conflictResolutionStrategy) insertCallback?.invoke(rowIds) } - } /** @@ -105,18 +113,17 @@ class Dao( */ fun List.insertWithDataCallback( conflictResolutionStrategy: ConflictResolutionStrategy = ConflictResolutionStrategy.CONFLICT_NONE, - insertCallback: ((rowIds: List, data: List) -> Unit)? = null + insertCallback: ((rowIds: List, data: List) -> Unit)? = null, ) { runTransactionOrDeferToCreation { db: SQLiteDatabase -> val (rowIds, insertedData) = insertData(db, this, conflictResolutionStrategy) insertCallback?.invoke(rowIds, insertedData) } - } - //will return null if db is not yet ready + // will return null if db is not yet ready fun List.insertSync( - conflictResolutionStrategy: ConflictResolutionStrategy = ConflictResolutionStrategy.CONFLICT_NONE + conflictResolutionStrategy: ConflictResolutionStrategy = ConflictResolutionStrategy.CONFLICT_NONE, ): List? { awaitDbInitialization() return (_db?.let { db -> @@ -124,44 +131,77 @@ class Dao( }) } + fun List.deleteSync(): Int { + val fields = entityClass.getAnnotation(RudderEntity::class.java)?.fields + + val args = findArgumentsFromEntities() + val whereClause = deleteClauseFromFields(fields, args) - fun List.delete(deleteCallback: ((numberOfRows: Int) -> Unit)? = null) { + return deleteSync(whereClause, null).also { + notifyDelete(it) + } + } + fun List.delete(deleteCallback: ((numberOfRows: Int) -> Unit)? = null) { runTransactionOrDeferToCreation { _ -> val fields = entityClass.getAnnotation(RudderEntity::class.java)?.fields - val args = map { - it.getPrimaryKeyValues().map { - "\"${it}\"" - } - }.reduce { acc, strings -> - acc.mapIndexed { index, s -> - - "$s, ${strings[index]}" - } - } - val whereClause = fields?.takeIf { - it.isNotEmpty() - }?.filter { - it.primaryKey - }?.mapIndexed { index, it -> - "${it.fieldName} IN (${args[index]})" - }?.reduce { acc, s -> "$acc AND $s" } + val args = findArgumentsFromEntities() + val whereClause = deleteClauseFromFields(fields, args) // receives the number of deleted rows and fires callback val extendedDeleteCb = { numberOfRows: Int -> deleteCallback?.invoke(numberOfRows) - if(_dataChangeListeners.isNotEmpty()) { - val allData = getAllSync() ?: listOf() - _dataChangeListeners.forEach { - it.onDataDeleted(this.subList(0, numberOfRows), allData) - } - } + notifyDelete(numberOfRows) } delete(whereClause, null, extendedDeleteCb) } } - //delete + + private fun List.notifyDelete(numberOfRows: Int) { + if (_dataChangeListeners.isNotEmpty()) { + _dataChangeListeners.forEach { + it.onDataDeleted(this.subList(0, numberOfRows)) + } + } + } + + private fun deleteClauseFromFields( + fields: Array?, args: List + ) = fields?.takeIf { + it.isNotEmpty() + }?.filter { + it.primaryKey + }?.mapIndexed { index, it -> + "${it.fieldName} IN (${args[index]})" + }?.reduce { acc, s -> "$acc AND $s" } + + private fun List.findArgumentsFromEntities() = map { + it.getPrimaryKeyValues().map { + "\"${it}\"" + } + }.reduce { acc, strings -> + acc.mapIndexed { index, s -> + + "$s, ${strings[index]}" + } + } + // delete + /** + * Delete based on where clause + * + * @param whereClause Used for selecting items for deletion + * @param args Substituting arguments if any, else null + * @return number of rows deleted + */ + fun deleteSync( + whereClause: String?, args: Array? + ): Int { + awaitDbInitialization() + return _db?.let { db -> + deleteFromDb(db, tableName, whereClause, args) + } ?: -1 + } /** * val args = map { it.getPrimaryKeyValues() @@ -178,7 +218,7 @@ class Dao( val allData = getAllSync() ?: listOf() _dataChangeListeners.forEach { - it.onDataDeleted(this.subList(0,numberOfRowsDel), allData) + it.onDataDeleted(this.subList(0,numberOfRowsDel)) } */ /** @@ -191,7 +231,7 @@ class Dao( fun delete( whereClause: String?, args: Array?, - deleteCallback: ((numberOfRows: Int) -> Unit)? = null + deleteCallback: ((numberOfRows: Int) -> Unit)? = null, ) { runTransactionOrDeferToCreation { db -> val deletedRows = deleteFromDb(db, tableName, whereClause, args) @@ -201,30 +241,36 @@ class Dao( internal fun deleteFromDb( database: SQLiteDatabase, - tableName: String, whereClause: String?, args: Array? + tableName: String, + whereClause: String?, + args: Array?, ): Int { - return if (useContentProvider) context.contentResolver.delete( - entityContentProviderUri.build(), - whereClause, - args - ) - else synchronized(DB_LOCK) { + return if (useContentProvider) { + context.contentResolver.delete( + entityContentProviderUri.build(), + whereClause, + args, + ) + } else synchronized(DB_LOCK) { database.openDatabase?.delete(tableName, whereClause, args) } ?: -1 } internal fun updateSync( - database: SQLiteDatabase, tableName: String, values: ContentValues?, + database: SQLiteDatabase, + tableName: String, + values: ContentValues?, selection: String?, - selectionArgs: Array? + selectionArgs: Array?, ): Int { - return if (useContentProvider) context.contentResolver.update( - entityContentProviderUri.build(), - values, - selection, - selectionArgs - ) - else synchronized(DB_LOCK) { + return if (useContentProvider) { + context.contentResolver.update( + entityContentProviderUri.build(), + values, + selection, + selectionArgs, + ) + } else synchronized(DB_LOCK) { database.openDatabase?.update(tableName, values, selection, selectionArgs) } ?: -1 } @@ -233,7 +279,6 @@ class Dao( runTransactionOrDeferToCreation { db: SQLiteDatabase -> callback.invoke(getItems(db, null)) } - } /** @@ -277,6 +322,9 @@ class Dao( } private fun awaitDbInitialization() { + while (_db == null){ + // busy wait until _db is assigned + } todoLock.lock() todoLock.unlock() } @@ -292,7 +340,8 @@ class Dao( ) { runTransactionOrDeferToCreation { _: SQLiteDatabase -> callback.invoke( - runGetQuerySync(columns, selection, selectionArgs, orderBy, limit, offset) ?: listOf() + runGetQuerySync(columns, selection, selectionArgs, orderBy, limit, offset) + ?: listOf(), ) } } @@ -318,7 +367,7 @@ class Dao( selectionArgs, orderBy, limit, - offset + offset, ) } @@ -357,7 +406,7 @@ class Dao( } } - //create/update + // create/update private fun insertData( db: SQLiteDatabase, items: List, @@ -375,66 +424,70 @@ class Dao( private fun processEntityInsertion( db: SQLiteDatabase, conflictResolutionStrategy: ConflictResolutionStrategy, - items: List + items: List, ): Pair, List> { var (autoIncrementFieldName: String?, nextValue: Long) = getAutoIncrementFieldToNextValue(db) var dbCount = - if (conflictResolutionStrategy == ConflictResolutionStrategy.CONFLICT_IGNORE) getCountSync( - db - ) else 0L + if (conflictResolutionStrategy == ConflictResolutionStrategy.CONFLICT_IGNORE) { + getCountSync( + db, + ) + } else { + 0L + } var rowIds = listOf() var returnedItems = listOf() - if (!useContentProvider) - synchronized(DB_LOCK) { - items.forEach { - val contentValues = it.generateContentValues() - if (autoIncrementFieldName != null) { - contentValues.put(autoIncrementFieldName, nextValue) - } + synchronized(DB_LOCK) { + items.forEach { + val contentValues = it.generateContentValues() + if (autoIncrementFieldName != null) { + contentValues.put(autoIncrementFieldName, nextValue) + } - val insertedRowId = insertContentValues( - db, - tableName, - contentValues, - null, - conflictResolutionStrategy.type - ).let { - if (conflictResolutionStrategy == ConflictResolutionStrategy.CONFLICT_IGNORE) { - getInsertedRowIdForConflictIgnore(dbCount, it) - } else it - }.also { - if (it >= 0) { - nextValue++ - dbCount++ - } + val insertedRowId = insertContentValues( + db, + tableName, + contentValues, + null, + conflictResolutionStrategy.type, + ).let { + if (conflictResolutionStrategy == ConflictResolutionStrategy.CONFLICT_IGNORE) { + getInsertedRowIdForConflictIgnore(dbCount, it) + } else { + it + } + }.also { + if (it >= 0) { + nextValue++ + dbCount++ } - rowIds = rowIds + insertedRowId - returnedItems = - returnedItems + (if (insertedRowId < 0) it else contentValues.toEntity( - entityClass - )) } + rowIds = rowIds + insertedRowId + returnedItems = returnedItems + (if (insertedRowId < 0) { + it + } else contentValues.toEntity( + entityClass, + )) } + } if (returnedItems.isNotEmpty() && _dataChangeListeners.isNotEmpty()) { - val allData = getAllSync() ?: listOf() _dataChangeListeners.forEach { - it.onDataInserted(returnedItems.filterNotNull(), allData) + it.onDataInserted(returnedItems.filterNotNull()) } } return rowIds to returnedItems } - //we consider one key which is auto increment. - //consider only one auto increment key + // we consider one key which is auto increment. + // consider only one auto increment key private fun getAutoIncrementFieldToNextValue(db: SQLiteDatabase) = fields.firstOrNull { - it.type == RudderField.Type.INTEGER && - it.isAutoInc /*&& !it.primaryKey*/ + it.type == RudderField.Type.INTEGER && it.isAutoInc /*&& !it.primaryKey*/ }?.let { autoIncField -> autoIncField.fieldName to getMaxIntValueForColumn( db, tableName, - autoIncField.fieldName + autoIncField.fieldName, ) + 1L } ?: (null to 0L) @@ -446,14 +499,22 @@ class Dao( return keySet().associateWith { get(it) } } - //this method considers database is open and is available for query + // this method considers database is open and is available for query // -1 if no value present private fun getMaxIntValueForColumn( db: SQLiteDatabase, tableName: String, - column: String + column: String, ): Long { - return synchronized(DB_LOCK) { + return (if (useContentProvider) { + context.contentResolver.query( + entityContentProviderUri.build(), + arrayOf("IFNULL(MAX($column), 0)"), + null, + null, + null + ) + } else synchronized(DB_LOCK) { db.query( tableName, arrayOf("IFNULL(MAX($column), 0)"), @@ -461,45 +522,50 @@ class Dao( null, null, null, - null + null, ) - }.let { cursor -> + })?.let { cursor -> (if (cursor.moveToFirst()) { cursor.getLong(0) - } else -1).also { + } else { + -1 + }).also { cursor.close() } - } + } ?: -1 } internal fun insertContentValues( database: SQLiteDatabase, - tableName: String, contentValues: ContentValues, nullHackColumn: String?, - conflictAlgorithm: Int + tableName: String, + contentValues: ContentValues, + nullHackColumn: String?, + conflictAlgorithm: Int, ): Long { - - return if (useContentProvider) (context.contentResolver.insert( - entityContentProviderUri - .appendQueryParameter( + return if (useContentProvider) { + (context.contentResolver.insert( + entityContentProviderUri.appendQueryParameter( EntityContentProvider.ECP_CONFLICT_RESOLUTION_CODE, - conflictAlgorithm.toString() - ) - .build(), contentValues - )?.let { - it.lastPathSegment?.toLongOrNull() - } ?: -1) - else { + conflictAlgorithm.toString(), + ).appendQueryParameter( + EntityContentProvider.ECP_NULL_HACK_COLUMN_CODE, + conflictAlgorithm.toString(), + ).build(), + contentValues, + )?.let { + it.lastPathSegment?.toLongOrNull() + } ?: -1) + } else { (database.openDatabase?.insertWithOnConflict( tableName, nullHackColumn, contentValues, - conflictAlgorithm + conflictAlgorithm, ) ?: -1) } - } - //read + // read private fun getItems( db: SQLiteDatabase, columns: Array? = null, @@ -507,30 +573,35 @@ class Dao( selectionArgs: Array? = null, orderBy: String? = null, limit: String? = null, - offset: String? = null + offset: String? = null, ): List { - //have to use factory + // have to use factory val fields = entityClass.getAnnotation(RudderEntity::class.java)?.fields - ?: throw IllegalArgumentException("RudderEntity must have at least one field") - val cursor = ( - if (useContentProvider) context.contentResolver.query( - entityContentProviderUri - .appendQueryParameter(EntityContentProvider.ECP_LIMIT_CODE, limit).build(), - columns, selection, selectionArgs, orderBy + ?: throw IllegalArgumentException("RudderEntity must have at least one field") + val cursor = (if (useContentProvider) { + context.contentResolver.query( + entityContentProviderUri.appendQueryParameter( + EntityContentProvider.ECP_LIMIT_CODE, limit + ).build(), + columns, + selection, + selectionArgs, + orderBy, + ) + } else { + synchronized(DB_LOCK) { + db.openDatabase?.query( + tableName, + columns, + selection, + selectionArgs, + null, + null, + orderBy, + if (offset != null) "$offset,$limit" else limit, ) - else synchronized(DB_LOCK) { - db.openDatabase?.query( - tableName, - columns, - selection, - selectionArgs, - null, - null, - orderBy, - if (offset != null) "$offset,$limit" else limit - ) - }) ?: return listOf() - + } + }) ?: return listOf() val items = ArrayList(cursor.count) @@ -551,7 +622,6 @@ class Dao( // } } - private fun runTransactionOrDeferToCreation(queryTransaction: (SQLiteDatabase) -> Unit) { _db?.let { db -> awaitDbInitialization() @@ -572,7 +642,7 @@ class Dao( fun setDatabase(sqLiteDatabase: SQLiteDatabase?) { if (sqLiteDatabase == null) return - //run all pending tasks + // run all pending tasks executorService.execute { synchronized(DB_LOCK) { val tableStmt = createTableStmt(tableName, fields) @@ -583,14 +653,15 @@ class Dao( _db = sqLiteDatabase todoLock.lock() } - while (todoTransactions.isNotEmpty()){ + while (todoTransactions.isNotEmpty()) { try { executorService.takeUnless { it.isShutdown }?.submit( todoTransactions.poll( - 50, TimeUnit.MILLISECONDS - ) + 50, + TimeUnit.MILLISECONDS, + ), ) - }catch (ex: InterruptedException){ + } catch (ex: InterruptedException) { ex.printStackTrace() } } @@ -618,37 +689,33 @@ class Dao( _db?.openDatabase?.endTransaction() } - fun execTransaction(transaction : () -> Unit){ - synchronized(DB_LOCK){ + fun execTransaction(transaction: () -> Unit) { + synchronized(DB_LOCK) { beginTransaction() transaction.invoke() setTransactionSuccessful() endTransaction() } } - fun execSql(command: String, callback: (() -> Unit)? = null) { + fun execSql(command: String, callback: (() -> Unit)? = null) { runTransactionOrDeferToCreation { db: SQLiteDatabase -> synchronized(DB_LOCK) { - db.openDatabase?.execSQL(command) callback?.invoke() } - } - } private fun createTableStmt(tableName: String, fields: Array): String? { - val fieldsStmt = fields.map { - "'${it.fieldName}' ${it.type.notation}" + //field name and type - // if primary and auto increment - /*if (it.primaryKey && it.isAutoInc && it.type == RudderField.Type.INTEGER) " PRIMARY KEY AUTOINCREMENT" else "" +*/ - if (!it.isNullable || it.primaryKey) " NOT NULL" else "" //specifying nullability, primary key cannot be null + "'${it.fieldName}' ${it.type.notation}" + // field name and type + // if primary and auto increment + /*if (it.primaryKey && it.isAutoInc && it.type == RudderField.Type.INTEGER) " PRIMARY KEY AUTOINCREMENT" else "" +*/ + if (!it.isNullable || it.primaryKey) " NOT NULL" else "" // specifying nullability, primary key cannot be null }.reduce { acc, s -> "$acc, $s" } val primaryKeyStmt = - //auto increment is only available for one primary key + // auto increment is only available for one primary key fields.filter { it.primaryKey }.takeIf { !it.isNullOrEmpty() }?.map { it.fieldName }?.reduce { acc, s -> "$acc,$s" }?.let { @@ -661,8 +728,7 @@ class Dao( "UNIQUE($it)" } - return ("CREATE TABLE IF NOT EXISTS '$tableName' ($fieldsStmt ${if (primaryKeyStmt.isNotEmpty()) ", $primaryKeyStmt" else ""}" + - "${if (!uniqueKeyStmt.isNullOrEmpty()) ", $uniqueKeyStmt" else ""})") + return ("CREATE TABLE IF NOT EXISTS '$tableName' ($fieldsStmt ${if (primaryKeyStmt.isNotEmpty()) ", $primaryKeyStmt" else ""}" + "${if (!uniqueKeyStmt.isNullOrEmpty()) ", $uniqueKeyStmt" else ""})") } private fun createIndexStmt(tableName: String, fields: Array): String? { @@ -687,20 +753,25 @@ class Dao( } private fun RudderField.findValue(cursor: Cursor) = when (type) { - RudderField.Type.INTEGER -> if (isNullable) cursor.getLongOrNull( - cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: throw IllegalArgumentException("No such column $fieldName") - ) else cursor.getLong( + RudderField.Type.INTEGER -> if (isNullable) { + cursor.getLongOrNull( + cursor.getColumnIndex(fieldName).takeIf { it >= 0 } + ?: -1, + ) + } else cursor.getLong( cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: throw IllegalArgumentException("No such column $fieldName") + ?: throw IllegalArgumentException("No such column $fieldName"), ) - RudderField.Type.TEXT -> if (isNullable) cursor.getStringOrNull( - cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: throw IllegalArgumentException("No such column $fieldName")) - else cursor.getString( + RudderField.Type.TEXT -> if (isNullable) { + cursor.getStringOrNull( + cursor.getColumnIndex(fieldName).takeIf { it >= 0 } + ?: -1, + ) + } else cursor.getString( cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: throw IllegalArgumentException("No such column $fieldName")) + ?: throw IllegalArgumentException("No such column $fieldName"), + ) } private val SQLiteDatabase.openDatabase @@ -765,18 +836,17 @@ class Dao( } interface DataChangeListener { - fun onDataInserted(inserted: List, allData: List) { + fun onDataInserted(inserted: List) { /** * Implementation can be ignored */ } - fun onDataDeleted(deleted: List, allData: List) { + fun onDataDeleted(deleted: List) { /** * Implementation can be ignored */ } - } } @@ -794,5 +864,4 @@ private fun Cursor.getLongOrNull(colIndex: Int): Long? { } catch (_: Exception) { null } - } diff --git a/repository/src/main/java/com/rudderstack/android/repository/EntityContentProvider.kt b/repository/src/main/java/com/rudderstack/android/repository/EntityContentProvider.kt index 89dd84fb5..054dae926 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/EntityContentProvider.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/EntityContentProvider.kt @@ -21,6 +21,8 @@ import android.database.SQLException import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.net.Uri +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors /** @@ -29,11 +31,12 @@ import java.util.concurrent.Executors */ internal class EntityContentProvider : ContentProvider() { - companion object { + internal val ECP_NULL_HACK_COLUMN_CODE: String = "null_hack_column" internal const val ECP_ENTITY_CODE = "db_entity" internal const val ECP_LIMIT_CODE = "query_limit" + internal const val ECP_DATABASE_CODE = "db_name" internal const val ECP_CONFLICT_RESOLUTION_CODE = "ecp_conflict_resolution" private const val ECP_TABLE_URI_MATCHER_CODE = 1 private const val ECP_TABLE_SUB_QUERY_URI_MATCHER_CODE = 2 @@ -41,84 +44,136 @@ internal class EntityContentProvider : ContentProvider() { private var uriMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH) private var authority: String? = null internal val AUTHORITY - get() = authority ?: throw UninitializedPropertyAccessException("onAttachInfo not called yet") - - private var sqLiteOpenHelper: SQLiteOpenHelper? = null - + get() = authority + ?: throw UninitializedPropertyAccessException("onAttachInfo not called yet") + private var nameToRudderDatabaseMap = + ConcurrentHashMap>() + private var nameToCreateCommandMap = + ConcurrentHashMap>>() internal fun getContentUri(tableName: String, context: Context?): Uri { + println("authority52: $authority") if (context != null && authority == null) { authority = context.packageName + "." + EntityContentProvider::class.java.simpleName + println("authority: $authority") } val contentUri = Uri.parse("content://$authority/$tableName") try { - //https://developer.android.com/guide/topics/providers/content-provider-creating + // https://developer.android.com/guide/topics/providers/content-provider-creating uriMatcher.addURI( authority, tableName, - ECP_TABLE_URI_MATCHER_CODE + ECP_TABLE_URI_MATCHER_CODE, ) uriMatcher.addURI( authority, "$tableName/*", // *: Matches a string of any valid characters of any length. - ECP_TABLE_SUB_QUERY_URI_MATCHER_CODE + ECP_TABLE_SUB_QUERY_URI_MATCHER_CODE, ) } catch (e: Exception) { e.printStackTrace() } return contentUri } + + internal fun registerDatabase(database: RudderDatabase) { + val ref = reference.get() + if (ref != null) with(ref) { + database.populateNameToSqliteMapping() + } + nameToRudderDatabaseMap[database.databaseName] = WeakReference(database) + } + + internal fun registerTableCommands( + databaseName: String, + createTableStatement: String?, + createIndexStatement: String? + ) { + val commands = arrayOf(createTableStatement, createIndexStatement).filterNotNull().toTypedArray() + val ref = reference.get() + if (ref != null) with(ref) { + nameToSqLiteOpenHelperMap[databaseName]?.createTable( + commands + ) + }else{ + val databaseCommands = nameToCreateCommandMap[databaseName] + ?: mutableListOf>().also { nameToCreateCommandMap[databaseName] = it } + databaseCommands.add(commands) + } + } + private fun SQLiteOpenHelper.createTable(commands: Array){ + commands.forEach { + writableDatabase.execSQL(it) + } + } + + internal fun releaseDatabase(databaseName: String) { + nameToRudderDatabaseMap.remove(databaseName) + val removedHelper = reference.get()?.nameToSqLiteOpenHelperMap?.remove(databaseName) + if(removedHelper != null){ + try { + removedHelper.close() + }catch (ex: Exception){ + // ignore + } + } + } + + private var reference = WeakReference(null) + } - //we will be using this just to satisfy new Dao creation, however, the calls we make to Dao - //should be synchronous. + private var nameToSqLiteOpenHelperMap = ConcurrentHashMap() + + // we will be using this just to satisfy new Dao creation, however, the calls we make to Dao + // should be synchronous. private val _commonExecutor = Executors.newSingleThreadExecutor() override fun onCreate(): Boolean { - RudderDatabase.getDbDetails { name, version, dbUpgradeCb -> - sqLiteOpenHelper = object : SQLiteOpenHelper(context, name, null, version) { + reference = WeakReference(this) + nameToRudderDatabaseMap.forEach { + val attachedDatabase = it.value.get() + attachedDatabase?.populateNameToSqliteMapping() + } + nameToCreateCommandMap.forEach { + val attachedSQLiteOpenHelper = nameToSqLiteOpenHelperMap[it.key] + it.value.forEach { + attachedSQLiteOpenHelper?.createTable(it) + } + } + nameToCreateCommandMap.clear() + return true + } + + private fun RudderDatabase.populateNameToSqliteMapping() { + getDbDetails { name, version, dbCreatedCb, dbUpgradeCb -> + nameToSqLiteOpenHelperMap[name] = object : SQLiteOpenHelper(context, name, null, version) { init { - //listeners won't be fired else + // listeners won't be fired else writableDatabase } override fun onCreate(database: SQLiteDatabase?) { - /** - * empty implementation - */ + dbCreatedCb?.invoke(database) } override fun onUpgrade( database: SQLiteDatabase?, oldVersion: Int, - newVersion: Int + newVersion: Int, ) { dbUpgradeCb?.invoke(database, oldVersion, newVersion) } - } + } - return true } override fun attachInfo(context: Context?, info: ProviderInfo?) { - println("on attach info called: $info") - authority = info?.authority?: - info?.packageName?.let { it + "." + this@EntityContentProvider::class.java.simpleName} - - /*_uriMatcher?.addURI( - _authority, - EVENTS_TABLE_NAME, - com.rudderstack.android.sdk.core.EventContentProvider.EVENT_CODE - ) - _uriMatcher?.addURI( - _authority, - EVENTS_TABLE_NAME.toString() + "/#", - com.rudderstack.android.sdk.core.EventContentProvider.EVENT_ID_CODE - )*/ + authority = info?.authority + ?: info?.packageName?.let { it + "." + this@EntityContentProvider::class.java.simpleName } super.attachInfo(context, info) - } override fun query( @@ -126,39 +181,45 @@ internal class EntityContentProvider : ContentProvider() { projection: Array?, selection: String?, selectionArgs: Array?, - sortOrder: String? + sortOrder: String?, ): Cursor? { if (uriMatcher.match(uri) == -1) return null val tableName = uri.tableName ?: return null - return sqLiteOpenHelper?.writableDatabase?.query(tableName, projection, selection, - selectionArgs, null, null, sortOrder, uri.limit) + return uri.attachedSqliteOpenHelper?.writableDatabase?.query( + tableName, + projection, + selection, + selectionArgs, + null, + null, + sortOrder, + uri.limit, + ) } override fun getType(uri: Uri): String? { - return null //no mime types allowed + return null // no mime types allowed } - override fun insert(uri: Uri, values: ContentValues?): Uri? { if (uriMatcher.match(uri) == -1) return null - val dao = uri.initializedDao ?: return null - val tableName = uri.tableName ?: return null - val rowID = dao.insertContentValues( - sqLiteOpenHelper?.writableDatabase ?: return null, - tableName, values ?: return null, - null, uri.conflictAlgorithm?:SQLiteDatabase.CONFLICT_REPLACE - ) + val rowID = (uri.attachedSqliteOpenHelper?.writableDatabase?.insertWithOnConflict( + tableName, + uri.nullHackColumn, + values, + uri.conflictAlgorithm ?: SQLiteDatabase.CONFLICT_REPLACE, + ) ?: -1) /** * If record is added successfully */ if (rowID > 0) { val rowUri = ContentUris.withAppendedId( getContentUri(tableName, context), - rowID + rowID, ) context?.contentResolver?.notifyChange(rowUri, null) return rowUri @@ -169,25 +230,30 @@ internal class EntityContentProvider : ContentProvider() { override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { if (uriMatcher.match(uri) == -1) return -1 - val dao = uri.initializedDao ?: return -1 val tableName = uri.tableName ?: return -1 - return dao.deleteFromDb(sqLiteOpenHelper?.writableDatabase?:return -1, - tableName, selection, selectionArgs) + return uri.attachedSqliteOpenHelper?.writableDatabase?.delete( + tableName, + selection, + selectionArgs, + ) ?: -1 } override fun update( uri: Uri, values: ContentValues?, selection: String?, - selectionArgs: Array? + selectionArgs: Array?, ): Int { if (uriMatcher.match(uri) == -1) return -1 - val dao = uri.initializedDao ?: return -1 val tableName = uri.tableName ?: return -1 - return dao.updateSync(sqLiteOpenHelper?.writableDatabase?:return -1, - tableName,values, selection, selectionArgs) + return uri.attachedSqliteOpenHelper?.writableDatabase?.update( + tableName, + values, + selection, + selectionArgs, + ) ?: -1 } override fun onLowMemory() { @@ -195,22 +261,26 @@ internal class EntityContentProvider : ContentProvider() { super.onLowMemory() } - private val Uri.initializedDao: Dao? - get() { - val entity = getQueryParameter(ECP_ENTITY_CODE)?.let { - Class.forName(it) as? Class - } - return ((entity ?: return null)).let { - RudderDatabase.createNewDao(it, _commonExecutor) - }.also { dao -> - dao.setDatabase(sqLiteOpenHelper?.writableDatabase) - } - } private val Uri.tableName: String? get() = pathSegments[0] - private val Uri.limit : String? - get() = getQueryParameter(ECP_LIMIT_CODE) - - private val Uri.conflictAlgorithm : Int? - get() = getQueryParameter(ECP_CONFLICT_RESOLUTION_CODE)?.toIntOrNull() -} \ No newline at end of file + private val Uri.attachedSqliteOpenHelper: SQLiteOpenHelper? + get() = attachedDatabaseName?.let { + nameToSqLiteOpenHelperMap[it] ?: run { + attachedDatabase?.populateNameToSqliteMapping() + nameToSqLiteOpenHelperMap[it] + } + } + private val Uri.attachedDatabaseName: String? + get() = getQueryParameter(ECP_DATABASE_CODE) + private val Uri.attachedDatabase: RudderDatabase? + get() = attachedDatabaseName?.let { + nameToRudderDatabaseMap[it]?.get() + } + private val Uri.limit: String? + get() = getQueryParameter(ECP_LIMIT_CODE) + private val Uri.nullHackColumn: String? + get() = getQueryParameter(ECP_NULL_HACK_COLUMN_CODE) + + private val Uri.conflictAlgorithm: Int? + get() = getQueryParameter(ECP_CONFLICT_RESOLUTION_CODE)?.toIntOrNull() +} diff --git a/repository/src/main/java/com/rudderstack/android/repository/EntityFactory.kt b/repository/src/main/java/com/rudderstack/android/repository/EntityFactory.kt index 3be242686..6849224c9 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/EntityFactory.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/EntityFactory.kt @@ -26,8 +26,7 @@ interface EntityFactory { * @param T the type of object required * @param entity The class sent as T type is erased * @param values The values defined by the annotation @RudderEntity - * @see RudderEntity * @return an object of T */ fun getEntity(entity: Class, values: Map): T? -} \ No newline at end of file +} diff --git a/repository/src/main/java/com/rudderstack/android/repository/RudderDatabase.kt b/repository/src/main/java/com/rudderstack/android/repository/RudderDatabase.kt index a12601715..89ad1d432 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/RudderDatabase.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/RudderDatabase.kt @@ -18,6 +18,7 @@ import android.annotation.SuppressLint import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper +import java.io.File import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -25,98 +26,108 @@ import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit - /** - * Singleton class to act as the Database helper + * class to act as the Database helper */ -@SuppressLint("StaticFieldLeak") -object RudderDatabase { - private var sqliteOpenHelper: SQLiteOpenHelper? = null - private var database: SQLiteDatabase? = null - private var registeredDaoList : MutableMap, Dao> = - ConcurrentHashMap, Dao>() - private var context : Context? = null - private var useContentProvider = false - private var dbDetailsListeners = listOf<( - String, Int, - databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)? - ) -> Unit>() - private var databaseName: String? = null - private var databaseVersion: Int = 1 - - private var databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)? = null +private const val USE_CONTENT_PROVIDER_DEFAULT = false - private lateinit var commonExecutor : ExecutorService - - private lateinit var entityFactory: EntityFactory - - /** - * Initialize database - * - * @param context The context to create database - * @param databaseName The database name to be used for the App - * @param entityFactory Used to create entity from class name and values map - * @param version database version - * @param databaseCreatedCallback Can be used to prefill db on create - * @param databaseUpgradeCallback If db upgrade is necessary, this is to be handled - */ - fun init( - context: Context, databaseName: String, - entityFactory: EntityFactory, - useContentProvider: Boolean = this.useContentProvider, - version: Int = 1, - executorService: ExecutorService? = null, - databaseCreatedCallback: ((SQLiteDatabase?) -> Unit)? = null, - databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)? = null - ) { - commonExecutor = executorService ?: ThreadPoolExecutor( +@SuppressLint("StaticFieldLeak") +/** + * + * + * @property context The context to create database + * @property databaseName The database name to be used for the App + * @property entityFactory Used to create entity from class name and values map + * @property useContentProvider Use content provider to access database, required for apps with + * multiple processes + * @property databaseVersion database version + * @property databaseUpgradeCallback If db upgrade is necessary, this is to be handled + * @constructor + * + * + * @param providedExecutorService + * @param databaseCreatedCallback Can be used to prefill db on create + */ +class RudderDatabase( + private val context: Context, + internal val databaseName: String, + private val entityFactory: EntityFactory, + private val useContentProvider: Boolean = USE_CONTENT_PROVIDER_DEFAULT, + private val databaseVersion: Int = 1, + providedExecutorService: ExecutorService? = null, + private val databaseCreatedCallback: ((SQLiteDatabase?) -> Unit)? = null, + private var databaseUpgradeCallback: (( + SQLiteDatabase?, oldVersion: Int, + newVersion: + Int + ) -> Unit)? = null, +) { + + + private val commonExecutor: ExecutorService = + providedExecutorService.takeIf { it?.isShutdown == false } ?: ThreadPoolExecutor( 0, Int.MAX_VALUE, 60L, TimeUnit.SECONDS, - SynchronousQueue(), ThreadPoolExecutor.DiscardPolicy() - ); - this.entityFactory = entityFactory - if (sqliteOpenHelper != null) - return - this.useContentProvider = useContentProvider - this.context = context - this.databaseName = databaseName - this.databaseVersion = version - this.databaseUpgradeCallback = databaseUpgradeCallback - //calling the database name listeners + SynchronousQueue(), + ThreadPoolExecutor.DiscardPolicy(), + ) + private var sqliteOpenHelper: SQLiteOpenHelper? = null + private var database: SQLiteDatabase? = null + private var registeredDaoList: MutableMap, Dao> = + ConcurrentHashMap, Dao>() + private var dbDetailsListeners = listOf< + ( + String, + Int, + databaseCreatedCallback: ((SQLiteDatabase?) -> Unit)?, + databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)?, + ) -> Unit, + >() + + init { synchronized(this) { - dbDetailsListeners.forEach { it.invoke(databaseName, version, databaseUpgradeCallback) } - - sqliteOpenHelper = object : SQLiteOpenHelper(context, databaseName, null, version) { - init { - commonExecutor.execute { - this@RudderDatabase.database = writableDatabase - database?.let { - initDaoList(it, registeredDaoList.values.toList()) - } - } - - } - - override fun onCreate(database: SQLiteDatabase?) { + dbDetailsListeners.forEach { + it.invoke( + databaseName, + databaseVersion, + databaseCreatedCallback, + databaseUpgradeCallback + ) + } + if (!useContentProvider) { + sqliteOpenHelper = initializeSqlOpenHelper(databaseCreatedCallback) + } else { + EntityContentProvider.registerDatabase(this) + } + } + } - databaseCreatedCallback?.invoke(database) + private fun initializeSqlOpenHelper(databaseCreatedCallback: ((SQLiteDatabase?) -> Unit)?) = + object : SQLiteOpenHelper(context, databaseName, null, databaseVersion) { + init { + commonExecutor.execute { + this@RudderDatabase.database = writableDatabase + database?.let { + initDaoList(it, registeredDaoList.values.toList()) + } } + } - override fun onUpgrade( - database: SQLiteDatabase?, - oldVersion: Int, - newVersion: Int - ) { - databaseUpgradeCallback?.invoke(database, oldVersion, newVersion) - } + override fun onCreate(database: SQLiteDatabase?) { + databaseCreatedCallback?.invoke(database) + } + override fun onUpgrade( + database: SQLiteDatabase?, + oldVersion: Int, + newVersion: Int, + ) { + databaseUpgradeCallback?.invoke(database, oldVersion, newVersion) } } - } - /** * Get [Dao] for a particular [Entity] * @@ -127,13 +138,15 @@ object RudderDatabase { * @return A [Dao] based on the [entityClass] */ fun getDao( - entityClass: Class, executorService: ExecutorService = commonExecutor + entityClass: Class, + executorService: ExecutorService = commonExecutor, - ): Dao { + ): Dao { return registeredDaoList[entityClass]?.let { it as Dao } ?: createNewDao(entityClass, executorService) } + /** * Creates a new [Dao] object for an entity. * Usage of this method directly, is highly discouraged. @@ -144,11 +157,18 @@ object RudderDatabase { * @return */ internal fun createNewDao( - entityClass: Class, executorService: ExecutorService - - ): Dao = Dao(entityClass, useContentProvider, context?: - throw UninitializedPropertyAccessException("Did you call RudderDatabase.init?"), - entityFactory, executorService).also { + entityClass: Class, + executorService: ExecutorService, + + ): Dao = Dao( + entityClass, + useContentProvider, + context + ?: throw UninitializedPropertyAccessException("Did you call RudderDatabase.init?"), + entityFactory, + executorService, + databaseName + ).also { registeredDaoList[entityClass] = it database?.apply { initDaoList(this, listOf(it)) @@ -164,16 +184,15 @@ object RudderDatabase { */ internal fun getDbDetails( callback: ( - String, Int, - databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)? - ) -> Unit + String, + Int, + databaseCreatedCallback: ((SQLiteDatabase?) -> Unit)?, + databaseUpgradeCallback: ((SQLiteDatabase?, oldVersion: Int, newVersion: Int) -> Unit)?, + ) -> Unit, ) { - databaseName?.let { - callback.invoke(it, databaseVersion, databaseUpgradeCallback) + callback.invoke(it, databaseVersion, databaseCreatedCallback, databaseUpgradeCallback) } ?: synchronized(this) { dbDetailsListeners = dbDetailsListeners + callback } - - } fun Dao.unregister() { @@ -189,21 +208,35 @@ object RudderDatabase { } fun shutDown() { - if(context == null)return - registeredDaoList.clear() //clearing all cached dao - database?.apply{ - //synchronizing on database allows other database users to synchronize on the same + registeredDaoList.iterator().forEach { + it.value.setDatabase(null) + } + registeredDaoList.clear() // clearing all cached dao + sqliteOpenHelper?.apply { + // synchronizing on database allows other database users to synchronize on the same synchronized(this) { - sqliteOpenHelper?.close() - database?.close() + close() database = null } + } ?: run { + database?.close() + database = null + } + if (useContentProvider) { + EntityContentProvider.releaseDatabase(databaseName) } sqliteOpenHelper = null commonExecutor.shutdown() dbDetailsListeners = emptyList() databaseUpgradeCallback = null - useContentProvider = false } -} \ No newline at end of file + /** + * Deletes the database along with all the tables + * + */ + fun delete() { + val file = sqliteOpenHelper?.readableDatabase?.path?.let { File(it) } ?: return + SQLiteDatabase.deleteDatabase(file) + } +} diff --git a/repository/src/main/java/com/rudderstack/android/repository/annotation/RudderField.kt b/repository/src/main/java/com/rudderstack/android/repository/annotation/RudderField.kt index cc880eca5..2f713d0df 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/annotation/RudderField.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/annotation/RudderField.kt @@ -28,10 +28,13 @@ package com.rudderstack.android.repository.annotation */ annotation class RudderField( - val type: Type, val fieldName: String, val primaryKey: Boolean = false, + val type: Type, + val fieldName: String, + val primaryKey: Boolean = false, val isNullable: Boolean = true, - val isAutoInc: Boolean = false, val isIndex: Boolean = false, - val isUnique: Boolean = false + val isAutoInc: Boolean = false, + val isIndex: Boolean = false, + val isUnique: Boolean = false, ) { /** * Represents type of column @@ -39,7 +42,6 @@ annotation class RudderField( */ enum class Type(val notation: String) { INTEGER("INTEGER"), - TEXT("TEXT") + TEXT("TEXT"), } } - diff --git a/repository/src/main/java/com/rudderstack/android/repository/utils/SqliteConflictIgnoreIssueWorkAround.kt b/repository/src/main/java/com/rudderstack/android/repository/utils/SqliteConflictIgnoreIssueWorkAround.kt index ba932d2e0..f0bed0824 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/utils/SqliteConflictIgnoreIssueWorkAround.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/utils/SqliteConflictIgnoreIssueWorkAround.kt @@ -23,4 +23,4 @@ internal fun getInsertedRowIdForConflictIgnore(prevDbCount: Long, returnedRowId: return -1 } return returnedRowId -} \ No newline at end of file +} diff --git a/repository/src/test/java/com/rudderstack/android/repository/CustomEntityRudderDatabaseTest.kt b/repository/src/test/java/com/rudderstack/android/repository/CustomEntityRudderDatabaseTest.kt index dc0bffff8..eee53d5bd 100644 --- a/repository/src/test/java/com/rudderstack/android/repository/CustomEntityRudderDatabaseTest.kt +++ b/repository/src/test/java/com/rudderstack/android/repository/CustomEntityRudderDatabaseTest.kt @@ -19,11 +19,10 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.rudderstack.android.repository.annotation.RudderEntity import com.rudderstack.android.repository.annotation.RudderField -import com.rudderstack.android.repository.models.SampleEntity -import com.rudderstack.android.repository.models.TestEntityFactory import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestExecutor import org.awaitility.Awaitility import org.hamcrest.MatcherAssert +import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.After import org.junit.Before @@ -36,7 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean @Config(sdk = [29]) class CustomEntityRudderDatabaseTest { - //lets have a model class + // lets have a model class data class Model(val name: String, val values: Array) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -45,9 +44,7 @@ class CustomEntityRudderDatabaseTest { other as Model if (name != other.name) return false - if (!values.contentEquals(other.values)) return false - - return true + return values.contentEquals(other.values) } override fun hashCode(): Int { @@ -56,14 +53,15 @@ class CustomEntityRudderDatabaseTest { return result } } - //let's create an entity for the same + // let's create an entity for the same @RudderEntity( - "model_table", fields = [ + "model_table", + fields = [ RudderField(RudderField.Type.TEXT, "model_name", primaryKey = true), RudderField(RudderField.Type.TEXT, "model_values"), - ] + ], ) class ModelEntity(val model: Model) : Entity { companion object { @@ -71,15 +69,14 @@ class CustomEntityRudderDatabaseTest { return ModelEntity( Model( values["model_name"] as String, - (values["model_values"] as String).split(',').toTypedArray() - ) + (values["model_values"] as String).split(',').toTypedArray(), + ), ) } } override fun generateContentValues(): ContentValues { - return ContentValues( - ).also { + return ContentValues().also { it.put("model_name", model.name) it.put("model_values", model.values.reduce { acc, s -> "$acc,$s" }) } @@ -98,44 +95,45 @@ class CustomEntityRudderDatabaseTest { } } - //entity factory + // entity factory class ModelEntityFactory : EntityFactory { override fun getEntity(entity: Class, values: Map): T? { return when (entity) { ModelEntity::class.java -> ModelEntity.create(values) else -> null } as T? - } - } + lateinit var database: RudderDatabase @Before fun initialize() { - RudderDatabase.init( + database = RudderDatabase( ApplicationProvider.getApplicationContext(), // RuntimeEnvironment.application, - "testDb", ModelEntityFactory(), false, - executorService = TestExecutor() + "testDb", + ModelEntityFactory(), + false, + providedExecutorService = TestExecutor(), ) - } @After fun tearDown() { - RudderDatabase.shutDown() + database.shutDown() } + @Test - fun `test custom Entity`(){ + fun `test custom Entity`() { val sampleModelEntitiesToSave = listOf( - Model("name-1", arrayOf("a","b", "c")), - Model("name-2", arrayOf("d","e", "f")), - Model("name-3", arrayOf("g","h", "i")), - Model("name-4", arrayOf("j","k", "l")), + Model("name-1", arrayOf("a", "b", "c")), + Model("name-2", arrayOf("d", "e", "f")), + Model("name-3", arrayOf("g", "h", "i")), + Model("name-4", arrayOf("j", "k", "l")), ).map { ModelEntity(it) } - val entityModelDao = RudderDatabase.getDao(ModelEntity::class.java) - //save data + val entityModelDao = database.getDao(ModelEntity::class.java) + // save data val isCompleted = AtomicBoolean(false) with(entityModelDao) { val rowIds = sampleModelEntitiesToSave.insertSync() @@ -148,28 +146,53 @@ class CustomEntityRudderDatabaseTest { it.model.name } MatcherAssert.assertThat( - savedItems, Matchers.allOf( + savedItems, + Matchers.allOf( Matchers.iterableWithSize(4), - Matchers.contains(*namesToBePresent.toTypedArray()) - ) + Matchers.contains(*namesToBePresent.toTypedArray()), + ), ) sampleModelEntitiesToSave.subList(0, 2).delete() { - //number of deleted rows is 2 + // number of deleted rows is 2 MatcherAssert.assertThat(it, Matchers.equalTo(2)) val items = getAllSync()?.map { it.model.name } MatcherAssert.assertThat( - items, Matchers.allOf( + items, + Matchers.allOf( Matchers.iterableWithSize(2), - Matchers.contains(*namesToBePresent.subList(2,4).toTypedArray()) - ) + Matchers.contains(*namesToBePresent.subList(2, 4).toTypedArray()), + ), ) isCompleted.set(true) } } Awaitility.await().atMost(500, TimeUnit.SECONDS).untilTrue(isCompleted) + } + + //behaviour verification of insertOrIncrement + @Test + fun `test duplicate entity insertion`() { + val labelEntities = (0..50).map { + ModelEntity(Model("testLabel_key_$it", arrayOf("testLabel_value_$it"))) // unique + // elements + } + val modelDao = database.getDao(ModelEntity::class.java) + // we insert unique elements, so the size of the inserted ids should be 51 + with(modelDao) { + val insertedIds = + labelEntities.insertSync(conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE) + assertThat(insertedIds?.size, Matchers.equalTo(51)) + assertThat(insertedIds, Matchers.not(Matchers.contains(-1))) + // we try inserting the elements again. IGNORE strategy should ignore the insertions + val duplicateIds = + labelEntities.insertSync(conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE) + ?.toSet() + assertThat(duplicateIds?.size, Matchers.equalTo(1)) + assertThat(duplicateIds, Matchers.contains(-1)) + } } -} \ No newline at end of file +} diff --git a/repository/src/test/java/com/rudderstack/android/repository/RudderDatabaseTest.kt b/repository/src/test/java/com/rudderstack/android/repository/RudderDatabaseTest.kt index 3d3befe21..71342dfc9 100644 --- a/repository/src/test/java/com/rudderstack/android/repository/RudderDatabaseTest.kt +++ b/repository/src/test/java/com/rudderstack/android/repository/RudderDatabaseTest.kt @@ -33,37 +33,37 @@ import org.robolectric.annotation.Config import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean -//@RunWith(RobolectricTestRunner::class) +// @RunWith(RobolectricTestRunner::class) @RunWith(AndroidJUnit4::class) @Config(sdk = [29]) class RudderDatabaseTest { - // private lateinit var -// private val delayedExecutor = Executors.newSingleThreadExecutor() + private lateinit var database: RudderDatabase + + // private val delayedExecutor = Executors.newSingleThreadExecutor() @Before fun initialize() { - RudderDatabase.init( + database = RudderDatabase( ApplicationProvider.getApplicationContext(), "testDb", TestEntityFactory, false, - executorService = TestExecutor() + providedExecutorService = TestExecutor(), ) - } @After fun tearDown() { - RudderDatabase.shutDown() + database.shutDown() } @Test fun `test race condition in dao list initialisation`() { - - val sampleDao = RudderDatabase.createNewDao(SampleEntity::class.java, TestExecutor()) - val sampleAutoGenDao = RudderDatabase.createNewDao( - SampleAutoGenEntity::class.java, TestExecutor() + val sampleDao = database.createNewDao(SampleEntity::class.java, TestExecutor()) + val sampleAutoGenDao = database.createNewDao( + SampleAutoGenEntity::class.java, + TestExecutor(), ) - val sampleDaoCheck = RudderDatabase.getDao(SampleEntity::class.java) + val sampleDaoCheck = database.getDao(SampleEntity::class.java) MatcherAssert.assertThat(sampleDao, Matchers.equalTo(sampleDaoCheck)) MatcherAssert.assertThat(sampleAutoGenDao, Matchers.equalTo(sampleAutoGenDao)) // Thread.sleep(5000) @@ -71,8 +71,8 @@ class RudderDatabaseTest { @Test fun multipleDaoCallsToReturnSameDaoObject() { - val sampleDaoCheck = RudderDatabase.getDao(SampleEntity::class.java) - val sampleDao = RudderDatabase.getDao(SampleEntity::class.java) + val sampleDaoCheck = database.getDao(SampleEntity::class.java) + val sampleDao = database.getDao(SampleEntity::class.java) MatcherAssert.assertThat(sampleDao, Matchers.equalTo(sampleDaoCheck)) } @@ -80,88 +80,87 @@ class RudderDatabaseTest { fun testInsertionAndGetSync() { val sampleEntitiesToSave = listOf( SampleEntity("abc", 10, listOf("12", "34", "56")), - SampleEntity("def", 20, listOf("78", "90", "12")) + SampleEntity("def", 20, listOf("78", "90", "12")), ) - val sampleDao = RudderDatabase.getDao(SampleEntity::class.java) - //save data + val sampleDao = database.getDao(SampleEntity::class.java) + // save data val isInserted = AtomicBoolean(false) with(sampleDao) { sampleEntitiesToSave.insert() { rowIds -> assertThat(rowIds, iterableWithSize(2)) - println("inserted: ${rowIds.size}") isInserted.set(true) } } Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isInserted) - //getting the data + // getting the data val savedData = with(sampleDao) { getAllSync() } MatcherAssert.assertThat( - savedData, allOf( - Matchers.iterableWithSize(2), contains(*sampleEntitiesToSave.toTypedArray()) - ) + savedData, + allOf( + Matchers.iterableWithSize(2), + contains(*sampleEntitiesToSave.toTypedArray()), + ), ) - } + @Test fun testSyncInsertionAndGetSync() { val sampleEntitiesToSave = listOf( SampleEntity("abc", 10, listOf("12", "34", "56")), - SampleEntity("def", 20, listOf("78", "90", "12")) + SampleEntity("def", 20, listOf("78", "90", "12")), ) - val sampleDao = RudderDatabase.getDao(SampleEntity::class.java) - //save data + val sampleDao = database.getDao(SampleEntity::class.java) + // save data // val isInserted = AtomicBoolean(false) with(sampleDao) { val rowIds = sampleEntitiesToSave.insertSync() assertThat(rowIds, iterableWithSize(2)) - println("inserted: ${rowIds!!.size}") // isInserted.set(true) } // Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isInserted) - //getting the data + // getting the data val savedData = with(sampleDao) { getAllSync() } MatcherAssert.assertThat( - savedData, allOf( - Matchers.iterableWithSize(2), contains(*sampleEntitiesToSave.toTypedArray()) - ) + savedData, + allOf( + Matchers.iterableWithSize(2), + contains(*sampleEntitiesToSave.toTypedArray()), + ), ) - } @Test fun testGetAsync() { val sampleEntitiesToSave = listOf( SampleEntity("abc", 10, listOf("12", "34", "56")), - SampleEntity("def", 20, listOf("78", "90", "12")) + SampleEntity("def", 20, listOf("78", "90", "12")), ) - val sampleDao = RudderDatabase.getDao(SampleEntity::class.java) - //save data + val sampleDao = database.getDao(SampleEntity::class.java) + // save data val isInserted = AtomicBoolean(false) with(sampleDao) { val rowIds = sampleEntitiesToSave.insertSync() assertThat(rowIds, iterableWithSize(2)) - println("inserted: ${rowIds?.size}") isInserted.set(true) - } Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isInserted) val isGetComplete = AtomicBoolean(false) - //getting the data + // getting the data with(sampleDao) { getAll() { assertThat( - it, allOf( - Matchers.iterableWithSize(2), contains(*sampleEntitiesToSave.toTypedArray()) - ) + it, + allOf( + Matchers.iterableWithSize(2), + contains(*sampleEntitiesToSave.toTypedArray()), + ), ) isGetComplete.set(true) } } Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isGetComplete) - - } @Test @@ -169,63 +168,134 @@ class RudderDatabaseTest { val sampleEntitiesToSave = listOf( SampleEntity("abc", 10, listOf("12", "34", "56")), SampleEntity("fgh", 10, listOf("34", "56", "78")), - SampleEntity("def", 20, listOf("78", "90", "12")) + SampleEntity("def", 20, listOf("78", "90", "12")), ) - val sampleDao = RudderDatabase.getDao(SampleEntity::class.java) - //save data + val sampleDao = database.getDao(SampleEntity::class.java) + // save data val isCompleted = AtomicBoolean(false) with(sampleDao) { val rowIds = sampleEntitiesToSave.insertSync() assertThat(rowIds, iterableWithSize(3)) - println("inserted: ${rowIds?.size}") sampleEntitiesToSave.subList(0, 2).delete() { - //number of deleted rows is 2 + // number of deleted rows is 2 assertThat(it, equalTo(2)) val items = getAllSync() assertThat( - items, allOf( - iterableWithSize(1), contains(sampleEntitiesToSave[2]) - ) + items, + allOf( + iterableWithSize(1), + contains(sampleEntitiesToSave[2]), + ), ) isCompleted.set(true) } } Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isCompleted) + } + @Test + fun testDeleteSync() { + val sampleEntitiesToSave = listOf( + SampleEntity("abc", 10, listOf("12", "34", "56")), + SampleEntity("fgh", 10, listOf("34", "56", "78")), + SampleEntity("def", 20, listOf("78", "90", "12")), + ) + val sampleDao = database.getDao(SampleEntity::class.java) + with(sampleDao) { + val rowIds = sampleEntitiesToSave.insertSync() + assertThat(rowIds, iterableWithSize(3)) + + val rowsDeleted = sampleEntitiesToSave.subList(0, 2).deleteSync() + // number of deleted rows is 2 + assertThat(rowsDeleted, equalTo(2)) + val items = getAllSync() + assertThat( + items, + allOf( + iterableWithSize(1), + contains(sampleEntitiesToSave[2]), + ), + ) + + } } @Test fun testAutoGenEntities() { val entitiesToSave = listOf( - SampleAutoGenEntity("abc"), SampleAutoGenEntity("fgh"), SampleAutoGenEntity("def") + SampleAutoGenEntity("abc"), + SampleAutoGenEntity("fgh"), + SampleAutoGenEntity("def"), ) - val sampleDao = RudderDatabase.getDao(SampleAutoGenEntity::class.java) - //save data + val sampleDao = database.getDao(SampleAutoGenEntity::class.java) + // save data val isCompleted = AtomicBoolean(false) with(sampleDao) { val rowIds = entitiesToSave.insertSync() assertThat(rowIds, iterableWithSize(3)) - println("inserted: ${rowIds?.size}") assertThat(rowIds, iterableWithSize(3)) - //entities in db should have autogenerated ids + // entities in db should have autogenerated ids val savedEntities = getAllSync() assertThat(savedEntities, allOf(notNullValue(), iterableWithSize(3))) assertThat(savedEntities?.get(0), allOf(notNullValue(), hasProperty("id", not(0)))) savedEntities?.subList(0, 2)?.delete() { - //number of deleted rows is 2 + // number of deleted rows is 2 assertThat(it, equalTo(2)) val items = getAllSync() assertThat( - items, allOf( - iterableWithSize(1), contains(savedEntities[2]) - ) + items, + allOf( + iterableWithSize(1), + contains(savedEntities[2]), + ), ) isCompleted.set(true) } } Awaitility.await().atMost(10, TimeUnit.SECONDS).untilTrue(isCompleted) - } + @Test + fun `test multiple database instances`() { + val database1 = RudderDatabase( + ApplicationProvider.getApplicationContext(), + "testDb1", + TestEntityFactory, + false, + providedExecutorService = TestExecutor(), + ) + val database2 = RudderDatabase( + ApplicationProvider.getApplicationContext(), + "testDb2", + TestEntityFactory, + false, + providedExecutorService = TestExecutor(), + ) + val sampleDao1 = database1.getDao(SampleEntity::class.java) + val sampleDao2 = database2.getDao(SampleEntity::class.java) + val sampleEntitiesToSave = listOf( + SampleEntity("abc", 10, listOf("12", "34", "56")), + SampleEntity("def", 20, listOf("78", "90", "12")), + ) + // save data + val isInserted = AtomicBoolean(false) + with(sampleDao1) { + val rowIds = sampleEntitiesToSave.insertSync() + assertThat(rowIds, iterableWithSize(2)) + isInserted.set(true) + } + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(isInserted) + // getting the data + val savedData = with(sampleDao2) { getAllSync() } + + MatcherAssert.assertThat( + savedData, + allOf( + Matchers.iterableWithSize(0), + ), + ) + database1.shutDown() + database2.shutDown() + } } diff --git a/repository/src/test/java/com/rudderstack/android/repository/models/SampleEntity.kt b/repository/src/test/java/com/rudderstack/android/repository/models/SampleEntity.kt index 2fbed1b30..30b406172 100644 --- a/repository/src/test/java/com/rudderstack/android/repository/models/SampleEntity.kt +++ b/repository/src/test/java/com/rudderstack/android/repository/models/SampleEntity.kt @@ -36,7 +36,7 @@ data class SampleEntity( val count: Int, val items: List, ) : Entity { - companion object{ + companion object { const val TABLE_NAME = "sample" const val FIELD_NAME = "name" const val FIELD_COUNT = "count" diff --git a/repository/src/test/java/com/rudderstack/android/repository/models/TestEntityFactory.kt b/repository/src/test/java/com/rudderstack/android/repository/models/TestEntityFactory.kt index 773651e73..1590bc35a 100644 --- a/repository/src/test/java/com/rudderstack/android/repository/models/TestEntityFactory.kt +++ b/repository/src/test/java/com/rudderstack/android/repository/models/TestEntityFactory.kt @@ -19,14 +19,16 @@ import com.rudderstack.android.repository.EntityFactory object TestEntityFactory : EntityFactory { override fun getEntity(entity: Class, values: Map): T? { - return when(entity){ - SampleAutoGenEntity::class.java -> SampleAutoGenEntity( values["name"] as String).also { + return when (entity) { + SampleAutoGenEntity::class.java -> SampleAutoGenEntity(values["name"] as String).also { it.id = (values["id"] as Long).toInt() } - SampleEntity::class.java -> SampleEntity( - values["name"] as String, - (values["count"]).toString().toInt(), (values["items"] as String).split(',')) + SampleEntity::class.java -> SampleEntity( + values["name"] as String, + (values["count"]).toString().toInt(), + (values["items"] as String).split(','), + ) else -> null } as? T } -} \ No newline at end of file +} diff --git a/repository/src/test/java/com/rudderstack/android/repository/utils/TestExecutor.kt b/repository/src/test/java/com/rudderstack/android/repository/utils/TestExecutor.kt index b1539fd8b..061111457 100644 --- a/repository/src/test/java/com/rudderstack/android/repository/utils/TestExecutor.kt +++ b/repository/src/test/java/com/rudderstack/android/repository/utils/TestExecutor.kt @@ -20,16 +20,16 @@ import java.util.concurrent.TimeUnit class TestExecutor : AbstractExecutorService() { private var _isShutdown = false override fun execute(command: Runnable?) { -command?.run() + command?.run() } override fun shutdown() { - //No op + // No op _isShutdown = true } override fun shutdownNow(): MutableList { - // No op + // No op shutdown() return mutableListOf() } @@ -45,4 +45,4 @@ command?.run() override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { return false } -} \ No newline at end of file +} diff --git a/rudderjsonadapter/build.gradle b/rudderjsonadapter/build.gradle deleted file mode 100644 index 884951230..000000000 --- a/rudderjsonadapter/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Creator: Debanjan Chatterjee on 30/09/21, 11:41 PM Last modified: 30/09/21, 11:39 PM - * Copyright: All rights reserved Ⓒ 2021 http://rudderstack.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -compileKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.javaParameters = true -} -apply from : "$projectDir/../dependencies.gradle" - - - -dependencies { - - testImplementation 'junit:junit:4.+' - testImplementation deps.hamcrest -} -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/rudderjsonadapter/build.gradle.kts b/rudderjsonadapter/build.gradle.kts new file mode 100644 index 000000000..3e9e5b177 --- /dev/null +++ b/rudderjsonadapter/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-library") + id("kotlin") +} + +dependencies { + testImplementation(libs.junit) + testImplementation(libs.hamcrest) +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-jar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/rudderjsonadapter/src/main/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapter.kt b/rudderjsonadapter/src/main/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapter.kt index 29493d52b..54ce33f61 100644 --- a/rudderjsonadapter/src/main/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapter.kt +++ b/rudderjsonadapter/src/main/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapter.kt @@ -22,10 +22,10 @@ import java.lang.reflect.ParameterizedType * * @param T The generic type to be determined */ -abstract class RudderTypeAdapter { +abstract class RudderTypeAdapter { val type - get() = (this::class.java.genericSuperclass as? ParameterizedType)?.actualTypeArguments?.get(0) - companion object{ + get() = (this::class.java.genericSuperclass as? ParameterizedType)?.actualTypeArguments?.get(0) + companion object { /** * For ease of instantiation * ``` @@ -36,9 +36,7 @@ abstract class RudderTypeAdapter { * @param body Empty body to facilitate. Not used * @return [RudderTypeAdapter] */ - inline operator fun invoke( crossinline body : ()-> Unit) : RudderTypeAdapter = - object : RudderTypeAdapter(){} - + inline operator fun invoke(crossinline body: () -> Unit): RudderTypeAdapter = + object : RudderTypeAdapter() {} } - -} \ No newline at end of file +} diff --git a/rudderjsonadapter/src/test/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapterTest.kt b/rudderjsonadapter/src/test/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapterTest.kt index 92c4b9818..0c02ce5c1 100644 --- a/rudderjsonadapter/src/test/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapterTest.kt +++ b/rudderjsonadapter/src/test/java/com/rudderstack/rudderjsonadapter/RudderTypeAdapterTest.kt @@ -20,15 +20,16 @@ import org.junit.Test class RudderTypeAdapterTest { @Test - fun `test rudder type adapter returns correct type with Map`(){ - val typeAdapter = RudderTypeAdapter>{} + fun `test rudder type adapter returns correct type with Map`() { + val typeAdapter = RudderTypeAdapter> {} println(typeAdapter.type) assertThat(typeAdapter, Matchers.hasToString(Matchers.containsString("Map"))) } + @Test - fun `test rudder type adapter returns correct type with List`(){ - val typeAdapter = RudderTypeAdapter>{} + fun `test rudder type adapter returns correct type with List`() { + val typeAdapter = RudderTypeAdapter> {} println(typeAdapter.type) assertThat(typeAdapter, Matchers.hasToString(Matchers.containsString("List"))) } -} \ No newline at end of file +} diff --git a/rudderreporter/build.gradle b/rudderreporter/build.gradle deleted file mode 100644 index 5f6a58675..000000000 --- a/rudderreporter/build.gradle +++ /dev/null @@ -1,97 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'org.jetbrains.kotlin.android' -} -apply from : "$projectDir/../dependencies.gradle" -android { - namespace 'com.rudderstack.android.ruddermetricsreporterandroid' - compileSdk library.target_sdk - - defaultConfig { - minSdk library.min_sdk - targetSdk library.target_sdk - consumerProguardFiles 'proguard-rules.pro' - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - -// implementation deps.kotlinCore - implementation deps.androidXAnnotations - - api(project(path: projects.json_rudder_adapter)) - api(project(projects.repository)) - api(project(path: projects.web)) - api(project(path: projects.moshi_rudder_adapter)) - - compileOnly deps.gson - compileOnly deps.jackson - compileOnly deps.moshi.core - compileOnly deps.moshi.kotlin - compileOnly deps.moshi.adapter - - testImplementation(project(path: projects.moshi_rudder_adapter)) - testImplementation(project(path: projects.gson_rudder_adapter)) - testImplementation(project(path: projects.jackson_rudder_adapter)) - testImplementation deps.moshi.kotlin - testImplementation deps.moshi.core - testImplementation deps.gson - testImplementation deps.jackson - testImplementation deps.androidXTestExtJunitKtx - testImplementation deps.androidXTestRules - testImplementation deps.androidXTest - testImplementation deps.hamcrest - testImplementation deps.mockito - testImplementation deps.mockito_kotlin - testImplementation deps.awaitility - testImplementation deps.robolectric - testImplementation project(path: projects.repository) - - -// androidTestImplementation(project(path: projects.moshi_rudder_adapter)) -// androidTestImplementation(project(path: projects.gson_rudder_adapter)) - androidTestImplementation(project(path: projects.jackson_rudder_adapter)) - androidTestImplementation deps.moshi.kotlin - androidTestImplementation deps.moshi.core -// androidTestImplementation deps.gson -// androidTestImplementation deps.jackson - androidTestImplementation deps.androidXTestExtJunitKtx - androidTestImplementation deps.androidXTestRules - androidTestImplementation deps.androidXTest -// androidTestImplementation deps.hamcrest -// androidTestImplementation deps.mockito -// androidTestImplementation deps.mockito_kotlin - androidTestImplementation deps.androidXTestRunner -// androidTestImplementation('androidx.test.espresso:espresso-contrib:3.5.1') { -// exclude module: "protobuf-lite" -// } -} -apply from: rootProject.file('gradle/artifacts-aar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') \ No newline at end of file diff --git a/rudderreporter/build.gradle.kts b/rudderreporter/build.gradle.kts new file mode 100644 index 000000000..14f659c6f --- /dev/null +++ b/rudderreporter/build.gradle.kts @@ -0,0 +1,88 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("org.jetbrains.kotlin.android") +} + +android { + + namespace = "com.rudderstack.android.ruddermetricsreporterandroid" + compileSdk = RudderstackBuildConfig.Android.TARGET_SDK + + defaultConfig { + minSdk = RudderstackBuildConfig.Android.MIN_SDK + targetSdk = RudderstackBuildConfig.Android.TARGET_SDK + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + named("release") { + isMinifyEnabled = false + setProguardFiles( + listOf( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) + ) + } + } + compileOptions { + sourceCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + targetCompatibility = RudderstackBuildConfig.Build.JAVA_VERSION + } + kotlinOptions { + jvmTarget = RudderstackBuildConfig.Build.JVM_TARGET + javaParameters = true + } + testOptions { + unitTests { + this.isIncludeAndroidResources = true + } + } +} + +dependencies { + + implementation(libs.android.x.annotation) + + api(project(":rudderjsonadapter")) + api(project(":repository")) + api(project(":web")) + api(project(":moshirudderadapter")) + + compileOnly(libs.gson) + compileOnly(libs.jackson.core) + compileOnly(libs.moshi) + compileOnly(libs.moshi.kotlin) + compileOnly(libs.moshi.adapters) + + testImplementation(project(":moshirudderadapter")) + testImplementation(project(":gsonrudderadapter")) + testImplementation(project(":jacksonrudderadapter")) + testImplementation(project(":repository")) + testImplementation(libs.android.x.test) + testImplementation(libs.android.x.testrules) + testImplementation(libs.android.x.test.ext.junitktx) + testImplementation(libs.awaitility) + testImplementation(libs.gson) + testImplementation(libs.jackson.core) + testImplementation(libs.hamcrest) + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.moshi) + testImplementation(libs.moshi.kotlin) + testImplementation(libs.robolectric) + + androidTestImplementation(project(":jacksonrudderadapter")) + androidTestImplementation(libs.android.x.test) + androidTestImplementation(libs.android.x.testrules) + androidTestImplementation(libs.android.x.testrunner) + androidTestImplementation(libs.android.x.test.ext.junitktx) + androidTestImplementation(libs.moshi) + androidTestImplementation(libs.moshi.kotlin) +} + +apply(from = "${project.projectDir.parentFile}/gradle/artifacts-aar.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/mvn-publish.gradle") +apply(from = "${project.projectDir.parentFile}/gradle/codecov.gradle") diff --git a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleInstrumentedTest.kt b/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleInstrumentedTest.kt deleted file mode 100644 index f9a8bacb3..000000000 --- a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.rudderstack.android.ruddermetricsreporterandroid - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rudderstack.android.ruddermetricsreporterandroid.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorClientTest.kt b/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorClientTest.kt index 3203a4354..1c5d3887f 100644 --- a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorClientTest.kt +++ b/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorClientTest.kt @@ -4,9 +4,9 @@ * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a - * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. @@ -23,7 +23,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.TestUtils.gene import com.rudderstack.android.ruddermetricsreporterandroid.error.TestUtils.generateLibraryMetadata import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir import com.rudderstack.jacksonrudderadapter.JacksonAdapter -import com.rudderstack.moshirudderadapter.MoshiAdapter import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -34,10 +33,6 @@ import org.junit.Before import org.junit.Test import java.util.Arrays import java.util.Collections -import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executor -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class ErrorClientTest { @@ -46,6 +41,7 @@ class ErrorClientTest { private var client: DefaultErrorClient? = null private val jsonAdapter = JacksonAdapter() + /** * Generates a configuration and clears sharedPrefs values to begin the test with a clean slate */ @@ -60,8 +56,8 @@ class ErrorClientTest { */ @After fun tearDown() { - client?.close() - client = null + client?.close() + client = null } @Test @@ -70,11 +66,12 @@ class ErrorClientTest { client = TestUtils.generateClient(Configuration(generateLibraryMetadata()), jsonAdapter) client?.notify(RuntimeException("Testing")) } + @Test fun testMaxErrors() { val config: Configuration = generateConfiguration() config.maxPersistedEvents = 2 - val reservoir = DefaultReservoir(context!!, false) + val reservoir = DefaultReservoir(context!!, false, "test_db") reservoir.clearErrors() client = TestUtils.generateClient(config, reservoir, jsonAdapter) reservoir.assertErrorSize(0) @@ -92,24 +89,24 @@ class ErrorClientTest { try { assertEquals(expectedSize, it) countBlocker.set(false) - - }catch (ex: Exception){ + } catch (ex: Exception) { ex.printStackTrace() } } block(countBlocker) } - private fun block(condition: AtomicBoolean){ - while(condition.get()) { + private fun block(condition: AtomicBoolean) { + while (condition.get()) { try { - //busy block + // busy block Thread.sleep(10) } catch (ex: Exception) { ex.printStackTrace() } } } + @Test fun testMaxBreadcrumbs() { val config: Configuration = generateConfiguration() @@ -173,7 +170,7 @@ class ErrorClientTest { client?.leaveBreadcrumb("Foo") assertEquals( breadcrumbCount?.toLong(), - breadcrumbs?.size?.toLong() + breadcrumbs?.size?.toLong(), ) // should not pick up new breadcrumbs } @@ -189,7 +186,6 @@ class ErrorClientTest { assertEquals("Manual breadcrumb", client?.breadcrumbState?.copy()?.get(0)?.name) } - @Test fun testAppDataMetadata() { client = generateClient(Configuration(generateLibraryMetadata()), jsonAdapter) @@ -203,7 +199,6 @@ class ErrorClientTest { assertNotNull(app["memoryTrimLevel"]) } - @Test fun testPopulateDeviceMetadata() { client = generateClient(Configuration(generateLibraryMetadata()), jsonAdapter) @@ -273,6 +268,4 @@ class ErrorClientTest { // // wait for all events to be delivered // assertTrue(latch.await(5, TimeUnit.SECONDS)) // } - - -} \ No newline at end of file +} diff --git a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/TestUtils.kt b/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/TestUtils.kt index 406b464d5..031f97816 100644 --- a/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/TestUtils.kt +++ b/rudderreporter/src/androidTest/java/com/rudderstack/android/ruddermetricsreporterandroid/error/TestUtils.kt @@ -4,18 +4,9 @@ import androidx.test.core.app.ApplicationProvider import com.rudderstack.android.ruddermetricsreporterandroid.Configuration import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir -import com.rudderstack.android.ruddermetricsreporterandroid.internal.App -import com.rudderstack.android.ruddermetricsreporterandroid.internal.AppWithState -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DeviceBuildInfo.Companion.defaultInfo -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DeviceWithState +import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir import com.rudderstack.android.ruddermetricsreporterandroid.internal.NoopLogger -import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ConfigModule -import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ContextModule -import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.ImmutableConfig import com.rudderstack.rudderjsonadapter.JsonAdapter -import java.io.File -import java.io.IOException -import java.util.Date internal object TestUtils { val runtimeVersions = HashMap() @@ -25,20 +16,27 @@ internal object TestUtils { runtimeVersions["androidApiLevel"] = "24" } - fun generateClient(configuration: Configuration, - jsonAdapter: JsonAdapter): DefaultErrorClient { - return DefaultErrorClient(ApplicationProvider.getApplicationContext(), configuration, jsonAdapter) - } - fun generateClient(configuration: Configuration, - reservoir: Reservoir, - jsonAdapter: JsonAdapter): DefaultErrorClient { + fun generateClient( + configuration: Configuration, + jsonAdapter: JsonAdapter, + ): DefaultErrorClient { return DefaultErrorClient(ApplicationProvider.getApplicationContext(), configuration, - reservoir, + DefaultReservoir(ApplicationProvider.getApplicationContext(), false, + "test_db"), jsonAdapter) } - - - + fun generateClient( + configuration: Configuration, + reservoir: Reservoir, + jsonAdapter: JsonAdapter, + ): DefaultErrorClient { + return DefaultErrorClient( + ApplicationProvider.getApplicationContext(), + configuration, + reservoir, + jsonAdapter, + ) + } fun generateConfiguration(): Configuration { val configuration = Configuration(generateLibraryMetadata()) @@ -90,4 +88,4 @@ internal object TestUtils { // fun generateApp(): App { // return App(generateImmutableConfig(), null, null, null, null, null) // } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Configuration.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Configuration.kt index 49fc2fbb8..14e3ec932 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Configuration.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Configuration.kt @@ -32,6 +32,7 @@ class Configuration(var libraryMetadata: LibraryMetadata) { set(value) { field = value ?: DebugLogger } + // var delivery: Delivery? = null var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS @@ -39,8 +40,7 @@ class Configuration(var libraryMetadata: LibraryMetadata) { var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS var maxStringValueLength: Int = DEFAULT_MAX_STRING_VALUE_LENGTH - - var crashFilter : CrashFilter? = null + var crashFilter: CrashFilter? = null var discardClasses: Set = emptySet() var enabledReleaseStages: Set? = null @@ -63,7 +63,5 @@ class Configuration(var libraryMetadata: LibraryMetadata) { private const val DEFAULT_MAX_REPORTED_THREADS = 200 private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000 private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000 - - } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt index 43532a1de..6edc339f3 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt @@ -19,7 +19,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorClient import com.rudderstack.android.ruddermetricsreporterandroid.internal.BackgroundTaskService import com.rudderstack.android.ruddermetricsreporterandroid.internal.Connectivity import com.rudderstack.android.ruddermetricsreporterandroid.internal.ConnectivityCompat -import com.rudderstack.android.ruddermetricsreporterandroid.internal.CustomDateAdapterMoshi import com.rudderstack.android.ruddermetricsreporterandroid.internal.DataCollectionModule import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultMetrics import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir @@ -55,17 +54,17 @@ class DefaultRudderReporter( networkExecutor: ExecutorService? = null, backgroundTaskService: BackgroundTaskService? = null, useContentProvider: Boolean = false, - isGzipEnabled: Boolean = true + isGzipEnabled: Boolean = true, ) : this( ContextModule(context), baseUrl, configuration, jsonAdapter, isMetricsEnabled, isErrorEnabled, - networkExecutor?:Executors.newCachedThreadPool(), + networkExecutor ?: Executors.newCachedThreadPool(), backgroundTaskService ?: BackgroundTaskService(), useContentProvider, - isGzipEnabled + isGzipEnabled, ) internal constructor( @@ -78,7 +77,7 @@ class DefaultRudderReporter( networkExecutor: ExecutorService = Executors.newCachedThreadPool(), backgroundTaskService: BackgroundTaskService? = null, useContentProvider: Boolean = false, - isGzipEnabled: Boolean = true + isGzipEnabled: Boolean = true, ) : this( contextModule, MemoryTrimState(), @@ -91,7 +90,7 @@ class DefaultRudderReporter( useContentProvider, isMetricsEnabled, isErrorEnabled, - isGzipEnabled + isGzipEnabled, ) constructor( @@ -112,10 +111,9 @@ class DefaultRudderReporter( MemoryTrimState(), isMetricsEnabled, isErrorEnabled, - backgroundTaskService + backgroundTaskService, ) - internal constructor( contextModule: ContextModule, memoryTrimState: MemoryTrimState, @@ -128,18 +126,23 @@ class DefaultRudderReporter( useContentProvider: Boolean, isMetricsAggregatorEnabled: Boolean, isErrorEnabled: Boolean, - isGzipEnabled: Boolean + isGzipEnabled: Boolean, ) : this( contextModule, - DefaultReservoir(contextModule.ctx, useContentProvider), + DefaultReservoir(contextModule.ctx, useContentProvider, configuration.libraryMetadata.writeKey), configuration, - DefaultUploadMediator(configModule, baseUrl, jsonAdapter, networkExecutor, - isGzipEnabled = isGzipEnabled), + DefaultUploadMediator( + configModule, + baseUrl, + jsonAdapter, + networkExecutor, + isGzipEnabled = isGzipEnabled, + ), jsonAdapter, memoryTrimState, isMetricsAggregatorEnabled, isErrorEnabled, - backgroundTaskService + backgroundTaskService, ) internal constructor( @@ -151,7 +154,7 @@ class DefaultRudderReporter( memoryTrimState: MemoryTrimState, isMetricsEnabled: Boolean = true, isErrorEnabled: Boolean = true, - backgroundTaskService: BackgroundTaskService? = null + backgroundTaskService: BackgroundTaskService? = null, ) : this( contextModule, reservoir, @@ -162,7 +165,7 @@ class DefaultRudderReporter( memoryTrimState, isMetricsEnabled, isErrorEnabled, - backgroundTaskService + backgroundTaskService, ) private constructor( @@ -175,11 +178,13 @@ class DefaultRudderReporter( memoryTrimState: MemoryTrimState, isMetricsEnabled: Boolean = true, isErrorEnabled: Boolean = true, - backgroundTaskService: BackgroundTaskService? = null - ):this(contextModule, reservoir, configuration, configModule, syncer, jsonAdapter, + backgroundTaskService: BackgroundTaskService? = null, + ) : this( + contextModule, reservoir, configuration, configModule, syncer, jsonAdapter, memoryTrimState, ConnectivityCompat(contextModule.ctx, RudderReporterNetworkChangeCallback(syncer)), - isMetricsEnabled, isErrorEnabled, backgroundTaskService) + isMetricsEnabled, isErrorEnabled, backgroundTaskService, + ) private constructor( contextModule: ContextModule, @@ -192,20 +197,27 @@ class DefaultRudderReporter( connectivity: Connectivity, isMetricsEnabled: Boolean = true, isErrorEnabled: Boolean = true, - backgroundTaskService: BackgroundTaskService? = null - ): this( + backgroundTaskService: BackgroundTaskService? = null, + ) : this( DefaultMetrics(DefaultAggregatorHandler(reservoir, isMetricsEnabled), syncer), DefaultErrorClient( - contextModule, configuration, configModule, DataCollectionModule( + contextModule, + configuration, + configModule, + DataCollectionModule( contextModule, configModule, SystemServiceModule(contextModule), backgroundTaskService ?: BackgroundTaskService(), connectivity, - memoryTrimState - ), reservoir, jsonAdapter, memoryTrimState, isErrorEnabled + memoryTrimState, + ), + reservoir, + jsonAdapter, + memoryTrimState, + isErrorEnabled, ), - syncer + syncer, ) { this.connectivity = connectivity this.backgroundTaskService = backgroundTaskService @@ -216,7 +228,7 @@ class DefaultRudderReporter( override val errorClient: ErrorClient get() = _errorClient ?: throw IllegalStateException( - "ErrorClient is not initialized. " + "Using deprecated constructor?" + "ErrorClient is not initialized. " + "Using deprecated constructor?", ) override fun shutdown() { @@ -225,12 +237,10 @@ class DefaultRudderReporter( connectivity?.unregisterForNetworkChanges() } - - //call unregister on shutdown + // call unregister on shutdown internal class RudderReporterNetworkChangeCallback(private val syncer: Syncer) : NetworkChangeCallback { override fun invoke(hasConnection: Boolean, networkState: String) { - if (hasConnection) { try { syncer.flushAllMetrics() @@ -250,5 +260,3 @@ class DefaultRudderReporter( // } // } } - - diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/JSerialize.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/JSerialize.kt index 4a12cdf90..e322e90cd 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/JSerialize.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/JSerialize.kt @@ -18,4 +18,4 @@ import com.rudderstack.rudderjsonadapter.JsonAdapter interface JSerialize { fun serialize(jsonAdapter: JsonAdapter): String? -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadata.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadata.kt index 9ccdcb937..fdbe8e8c0 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadata.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadata.kt @@ -40,9 +40,9 @@ data class LibraryMetadata @JvmOverloads constructor( @get:JsonProperty("os_version") @SerializedName("os_version") @Json(name = "os_version") - val osVersion: String = Build.VERSION.SDK_INT.toString() + val osVersion: String = Build.VERSION.SDK_INT.toString(), ) : JSerialize { override fun serialize(jsonAdapter: JsonAdapter): String? { return jsonAdapter.writeToJson(this) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Metrics.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Metrics.kt index a06221b0a..c24f65b34 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Metrics.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Metrics.kt @@ -14,14 +14,14 @@ package com.rudderstack.android.ruddermetricsreporterandroid -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.Counter import com.rudderstack.android.ruddermetricsreporterandroid.metrics.LongCounter import com.rudderstack.android.ruddermetricsreporterandroid.metrics.Meter interface Metrics { fun getMeter(): Meter + @Deprecated("Use [RudderReporter.syncer] instead") - fun getSyncer():Syncer + fun getSyncer(): Syncer /** * Enables or disables recording of metrics. However unless shut down, already recorded @@ -39,4 +39,4 @@ interface Metrics { @Deprecated("Use [RudderReporter.shutdown] instead") fun shutdown() fun getLongCounter(name: String): LongCounter = getMeter().longCounter(name) -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt index 41874a62e..c49b14685 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt @@ -21,16 +21,21 @@ import com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity interface Reservoir { fun insertOrIncrement(metric: MetricModel) fun getAllMetricsSync(): List> - fun getAllMetrics(callback : (List>) -> Unit) + fun getAllMetrics(callback: (List>) -> Unit) + + fun getMetricsFirstSync(limit: Long): List> + fun getMetricsFirst(skip: Long, limit: Long, callback: (List>) -> Unit) + fun getMetricsAndErrors( + skipForMetrics: Long, + skipForErrors: Long, + limit: Long, + callback: + (List>, List) -> Unit, + ) + fun getMetricsFirst(limit: Long, callback: (List>) -> Unit) - fun getMetricsFirstSync(limit : Long): List> - fun getMetricsFirst(skip: Long, limit : Long, callback : (List>) -> Unit) - fun getMetricsAndErrors(skipForMetrics: Long, skipForErrors: Long, limit : Long, callback : - (List>, List) -> Unit) - fun getMetricsFirst(limit : Long, callback : (List>) -> Unit) // fun getMetricsAndErrorFirst(limit : Long, callback : (List>, List) -> Unit) - fun getMetricsCount(callback : (Long) -> Unit) + fun getMetricsCount(callback: (Long) -> Unit) fun clear() fun clearMetrics() fun resetMetricsFirst(limit: Long) @@ -38,13 +43,17 @@ interface Reservoir { fun setMaxErrorCount(maxErrorCount: Long) fun saveError(errorEntity: ErrorEntity) fun getAllErrorsSync(): List - fun getAllErrors(callback : (List) -> Unit) + fun getAllErrors(callback: (List) -> Unit) - fun getErrorsFirstSync(limit : Long): List - fun getErrors(skip: Long, limit : Long, callback : (List) -> - Unit) - fun getErrorsFirst(limit : Long, callback : (List) -> Unit) - fun getErrorsCount(callback : (Long) -> Unit) + fun getErrorsFirstSync(limit: Long): List + fun getErrors( + skip: Long, + limit: Long, + callback: (List) -> + Unit, + ) + fun getErrorsFirst(limit: Long, callback: (List) -> Unit) + fun getErrorsCount(callback: (Long) -> Unit) fun clearErrors() fun clearErrors(ids: Array) @@ -65,7 +74,6 @@ interface Reservoir { * */ fun onDataChange() - } - //this is a combined response class -} \ No newline at end of file + // this is a combined response class +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt index e36b634fe..0f6adae4f 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt @@ -18,7 +18,7 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorClient interface RudderReporter { val metrics: Metrics - val errorClient : ErrorClient + val errorClient: ErrorClient val syncer: Syncer fun shutdown() -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt index 903a98f95..775811b20 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt @@ -19,15 +19,23 @@ import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel interface Syncer { fun startScheduledSyncs( - interval: Long, flushOnStart: Boolean, flushCount: Long + interval: Long, + flushOnStart: Boolean, + flushCount: Long, + ) + + // setting null will nullify the callback + fun setCallback( + callback: ( + ( + uploaded: List>, + uploadedErrorModel: ErrorModel, + success: Boolean, + ) -> Unit + )?, ) - //setting null will nullify the callback - fun setCallback(callback: ((uploaded: List>, - uploadedErrorModel: ErrorModel, - success: Boolean) -> Unit)?) fun stopScheduling() fun flushAllMetrics() - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt index 06fdc29d5..8b9bea11c 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt @@ -21,6 +21,6 @@ fun interface UploadMediator { fun upload( metrics: List>, error: ErrorModel, - callback: (success: Boolean) -> Unit + callback: (success: Boolean) -> Unit, ) -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbType.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbType.kt index 5ce6ee0a7..3076af79e 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbType.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbType.kt @@ -8,34 +8,42 @@ enum class BreadcrumbType(private val type: String) { * An error was sent to Bugsnag (internal use only) */ ERROR("error"), + /** * A log message */ LOG("log"), + /** * A manual invocation of `leaveBreadcrumb` (default) */ MANUAL("manual"), + /** * A navigation event, such as a window opening or closing */ NAVIGATION("navigation"), + /** * A background process such as a database query */ PROCESS("process"), + /** * A network request */ REQUEST("request"), + /** * A change in application state, such as launch or memory warning */ STATE("state"), + /** * A user action, such as tapping a button */ - USER("user"); + USER("user"), + ; override fun toString() = type diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilter.kt index 95c3bfcb4..ecf2fed64 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilter.kt @@ -16,17 +16,17 @@ package com.rudderstack.android.ruddermetricsreporterandroid.error fun interface CrashFilter { fun shouldKeep(exc: Throwable): Boolean - companion object{ - @JvmStatic - fun generateWithKeyWords(keyWords: List): CrashFilter { - return CrashFilter { exc -> - exc.isValid(keyWords) - } - } - private fun Throwable.isValid(keyWords: List) : Boolean{ - return keyWords.any { message?.contains(it) == true } - || keyWords.any { stackTraceToString().contains(it) } - || cause?.isValid(keyWords) == true + companion object { + @JvmStatic + fun generateWithKeyWords(keyWords: List): CrashFilter { + return CrashFilter { exc -> + exc.isValid(keyWords) + } + } + private fun Throwable.isValid(keyWords: List): Boolean { + return keyWords.any { message?.contains(it) == true } || + keyWords.any { stackTraceToString().contains(it) } || + cause?.isValid(keyWords) == true } } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/DefaultErrorClient.java b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/DefaultErrorClient.java index d9709b2ba..130e2f4fa 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/DefaultErrorClient.java +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/DefaultErrorClient.java @@ -34,7 +34,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.internal.ClientComponentCallbacks; import com.rudderstack.android.ruddermetricsreporterandroid.internal.ConnectivityCompat; import com.rudderstack.android.ruddermetricsreporterandroid.internal.DataCollectionModule; -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir; import com.rudderstack.android.ruddermetricsreporterandroid.internal.DeviceDataCollector; import com.rudderstack.android.ruddermetricsreporterandroid.internal.NoopLogger; import com.rudderstack.android.ruddermetricsreporterandroid.internal.StateObserver; @@ -166,12 +165,6 @@ MemoryTrimState getMemoryTrimState() { return memoryTrimState; } - public DefaultErrorClient(@NonNull Context context, - @NonNull Configuration configuration, - @NonNull JsonAdapter jsonAdapter) { - this(context, configuration, new DefaultReservoir(context, false), jsonAdapter); - } - public DefaultErrorClient(@NonNull Context context, @NonNull Configuration configuration, @NonNull Reservoir reservoir, diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEvent.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEvent.kt index 6a7fef793..4e0fbec93 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEvent.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEvent.kt @@ -30,7 +30,6 @@ import com.rudderstack.rudderjsonadapter.RudderTypeAdapter import com.squareup.moshi.FromJson import com.squareup.moshi.Json import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter import com.squareup.moshi.ToJson class ErrorEvent : MetadataAware, JSerialize { @@ -39,12 +38,18 @@ class ErrorEvent : MetadataAware, JSerialize { originalError: Throwable? = null, config: ImmutableConfig, severityReason: SeverityReason, - data: Metadata = Metadata() + data: Metadata = Metadata(), ) : this( - mutableListOf(), config.discardClasses.toSet(), when (originalError) { + mutableListOf(), + config.discardClasses.toSet(), + when (originalError) { null -> mutableListOf() else -> createError(originalError, config.projectPackages, config.logger) - }, data.copy(), originalError, config.projectPackages, severityReason + }, + data.copy(), + originalError, + config.projectPackages, + severityReason, ) internal constructor( @@ -55,7 +60,7 @@ class ErrorEvent : MetadataAware, JSerialize { originalError: Throwable? = null, projectPackages: Collection = setOf(), severityReason: SeverityReason = SeverityReason.newInstance( - SeverityReason.REASON_HANDLED_EXCEPTION + SeverityReason.REASON_HANDLED_EXCEPTION, ), ) { this.breadcrumbs = breadcrumbs @@ -114,7 +119,6 @@ class ErrorEvent : MetadataAware, JSerialize { var groupingHash: String? = null var context: String? = null - protected fun shouldDiscardClass(): Boolean { return when { errors.isEmpty() -> true @@ -160,7 +164,7 @@ class ErrorEvent : MetadataAware, JSerialize { severityReason.unhandled, severityReason.unhandledOverridden, severityReason.attributeValue, - severityReason.attributeKey + severityReason.attributeKey, ) } @@ -171,13 +175,12 @@ class ErrorEvent : MetadataAware, JSerialize { severityReason.unhandled, severityReason.unhandledOverridden, severityReason.attributeValue, - severityReason.attributeKey + severityReason.attributeKey, ) } fun getSeverityReasonType(): String = severityReason.severityReasonType - override fun addMetadata(section: String, value: Map) = metadata.addMetadata(section, value) @@ -192,22 +195,25 @@ class ErrorEvent : MetadataAware, JSerialize { override fun getMetadata(section: String, key: String) = metadata.getMetadata(section, key) override fun serialize(jsonAdapter: JsonAdapter): String? { - return jsonAdapter.writeToJson(mapOf( - "exceptions" to errors.map { it.toMap() }, - "severity" to severity, - "breadcrumbs" to breadcrumbs, - "context" to context, - "unhandled" to unhandled, - "projectPackages" to projectPackages, - "app" to app, - "device" to device.toMap(), - "metadata" to metadataMap/*.let { + return jsonAdapter.writeToJson( + mapOf( + "exceptions" to errors.map { it.toMap() }, + "severity" to severity, + "breadcrumbs" to breadcrumbs, + "context" to context, + "unhandled" to unhandled, + "projectPackages" to projectPackages, + "app" to app, + "device" to device.toMap(), + "metadata" to metadataMap,/*.let { var map: Map = mutableMapOf() it.forEach { (key, value) -> map = map + (key to value) } }*/ - ).filterValues { it != null }, object : RudderTypeAdapter>() {}) + ).filterValues { it != null }, + object : RudderTypeAdapter>() {}, + ) } override fun toString(): String { @@ -218,14 +224,15 @@ class ErrorEvent : MetadataAware, JSerialize { @FromJson fun fromJson( jsonReader: JsonReader, - delegate: com.squareup.moshi.JsonAdapter + delegate: com.squareup.moshi.JsonAdapter, ): ErrorEvent? { - //we don't need to serialize this class + // we don't need to serialize this class return ErrorEvent() } + @ToJson - fun toJson(errorEvent: ErrorEvent): Map{ - return mapOf( + fun toJson(errorEvent: ErrorEvent): Map { + return mapOf( "exceptions" to errorEvent.errors.map { it.toMap() }, "severity" to errorEvent.severity, "breadcrumbs" to errorEvent.breadcrumbs, @@ -234,7 +241,7 @@ class ErrorEvent : MetadataAware, JSerialize { "projectPackages" to errorEvent.projectPackages, "app" to errorEvent.app, "device" to errorEvent.device, - "metadata" to errorEvent.metadataMap/*.let { + "metadata" to errorEvent.metadataMap,/*.let { var map: Map = mutableMapOf() it.forEach { (key, value) -> map = map + (key to value) @@ -248,6 +255,5 @@ class ErrorEvent : MetadataAware, JSerialize { // writer.setLenient(wasSerializeNulls) // } } - } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModel.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModel.kt index 08892e87c..21049aab8 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModel.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModel.kt @@ -14,24 +14,25 @@ package com.rudderstack.android.ruddermetricsreporterandroid.error -import com.fasterxml.jackson.annotation.JsonIgnore import com.rudderstack.android.ruddermetricsreporterandroid.JSerialize import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.rudderjsonadapter.JsonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter -import com.squareup.moshi.Json class ErrorModel( private val libraryMetadata: LibraryMetadata, - internal val eventsJson: List) : JSerialize { + internal val eventsJson: List, +) : JSerialize { override fun serialize(jsonAdapter: JsonAdapter): String? { return jsonAdapter.writeToJson(toMap(jsonAdapter)) } - fun toMap(jsonAdapter: JsonAdapter) = mapOf("events" to + fun toMap(jsonAdapter: JsonAdapter) = mapOf( + "events" to eventsJson.map { - jsonAdapter.readJson(it, - RudderTypeAdapter>{} + jsonAdapter.readJson( + it, + RudderTypeAdapter> {}, ) }, "payloadVersion" to 5, @@ -39,7 +40,7 @@ class ErrorModel( "name" to libraryMetadata.name, "version" to libraryMetadata.sdkVersion, "url" to "https://github.com/rudderlabs/rudder-sdk-android", - "os_version" to libraryMetadata.osVersion - ) + "os_version" to libraryMetadata.osVersion, + ), ) -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/Metadata.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/Metadata.kt index a69e0e5f6..82c65fac1 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/Metadata.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/error/Metadata.kt @@ -16,7 +16,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.error -import com.rudderstack.android.ruddermetricsreporterandroid.JSerialize import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.MetadataAware import java.util.concurrent.ConcurrentHashMap @@ -26,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap * * Diagnostic information is presented on your Bugsnag dashboard in tabs. */ -data class Metadata @JvmOverloads constructor ( - internal val store: MutableMap> = ConcurrentHashMap() -) : MetadataAware{ +data class Metadata @JvmOverloads constructor( + internal val store: MutableMap> = ConcurrentHashMap(), +) : MetadataAware { override fun addMetadata(section: String, value: Map) { value.entries.forEach { @@ -111,7 +110,7 @@ data class Metadata @JvmOverloads constructor ( private fun getMergeValue( result: MutableMap, key: String, - map: Map + map: Map, ) { val baseValue = result[key] val overridesValue = map[key] @@ -134,10 +133,8 @@ data class Metadata @JvmOverloads constructor ( } fun copy(): Metadata { return this.copy(store = toMap()) - } - // fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics { // var stringCount = 0 // var charCount = 0 diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/App.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/App.kt index 70cd6c0c4..9bf273c24 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/App.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/App.kt @@ -2,7 +2,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.ImmutableConfig - /** * Stateless information set by the notifier about your app can be found on this class. These values * can be accessed and amended if necessary. @@ -33,11 +32,10 @@ open class App internal constructor( */ var codeBundleId: String?, - /** * The version code of the application set in [Configuration.versionCode] */ - var versionCode: String? + var versionCode: String?, ) { internal constructor( @@ -46,13 +44,13 @@ open class App internal constructor( id: String?, releaseStage: String?, version: String?, - codeBundleId: String? + codeBundleId: String?, ) : this( binaryArch, id, releaseStage, version, codeBundleId, - config.libraryMetadata.versionCode + config.libraryMetadata.versionCode, ) } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppDataCollector.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppDataCollector.kt index 1ce35aad1..c08d101a4 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppDataCollector.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppDataCollector.kt @@ -18,10 +18,9 @@ internal class AppDataCollector( private val packageManager: PackageManager?, private val config: ImmutableConfig, private val activityManager: ActivityManager?, - private val memoryTrimState: MemoryTrimState + private val memoryTrimState: MemoryTrimState, ) { - var codeBundleId: String? = null private val packageName: String = appContext.packageName @@ -40,7 +39,12 @@ internal class AppDataCollector( fun generateAppWithState(): AppWithState { return AppWithState( - config, binaryArch, packageName, releaseStage, versionName, codeBundleId + config, + binaryArch, + packageName, + releaseStage, + versionName, + codeBundleId, ) } @@ -111,8 +115,9 @@ internal class AppDataCollector( */ fun getInstallerPackageName(): String? { try { - if (VERSION.SDK_INT >= VERSION_CODES.R) + if (VERSION.SDK_INT >= VERSION_CODES.R) { return packageManager?.getInstallSourceInfo(packageName)?.installingPackageName + } @Suppress("DEPRECATION") return packageManager?.getInstallerPackageName(packageName) } catch (e: Exception) { diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppWithState.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppWithState.kt index 17694d256..1dacc2f3a 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppWithState.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/AppWithState.kt @@ -2,7 +2,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.ImmutableConfig - /** * Stateful information set by the notifier about your app can be found on this class. These values * can be accessed and amended if necessary. @@ -23,14 +22,13 @@ class AppWithState( id: String?, releaseStage: String?, version: String?, - codeBundleId: String? + codeBundleId: String?, ) : this( binaryArch, id, releaseStage, version, codeBundleId, - config.libraryMetadata.versionCode + config.libraryMetadata.versionCode, ) - } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/BackgroundTaskService.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/BackgroundTaskService.kt index f9937aa9f..8ce2fdd71 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/BackgroundTaskService.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/BackgroundTaskService.kt @@ -42,7 +42,7 @@ enum class TaskType { * short-lived operations that take <100ms, such as registering a * [android.content.BroadcastReceiver]. */ - DEFAULT + DEFAULT, } private const val SHUTDOWN_WAIT_MS = 1500L @@ -73,7 +73,7 @@ internal fun createExecutor(name: String, type: TaskType, keepAlive: Boolean): E KEEP_ALIVE_SECS, TimeUnit.SECONDS, queue, - threadFactory + threadFactory, ) } @@ -92,29 +92,29 @@ class BackgroundTaskService @JvmOverloads constructor( internal val errorExecutor: ExecutorService = createExecutor( "Rudder Error thread", TaskType.ERROR_REQUEST, - true + true, ), @get:VisibleForTesting internal val databaseExecutor: ExecutorService = createExecutor( "Rudder Database thread", TaskType.DB_REQUEST, - true + true, ), @get:VisibleForTesting internal val ioExecutor: ExecutorService = createExecutor( "Rudder IO thread", TaskType.IO, - true + true, ), @get:VisibleForTesting internal val defaultExecutor: ExecutorService = createExecutor( "Bugsnag Default thread", TaskType.DEFAULT, - false - ) + false, + ), ) { /** @@ -180,7 +180,7 @@ class BackgroundTaskService @JvmOverloads constructor( private class SafeFuture( private val delegate: FutureTask, - private val taskType: TaskType + private val taskType: TaskType, ) : Future by delegate { override fun get(): V { ensureTaskGetSafe() diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ClientComponentCallbacks.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ClientComponentCallbacks.kt index 833f68546..3b08a3920 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ClientComponentCallbacks.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ClientComponentCallbacks.kt @@ -6,7 +6,7 @@ import android.content.res.Configuration internal class ClientComponentCallbacks( private val deviceDataCollector: DeviceDataCollector, private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit, - val memoryCallback: (Boolean, Int?) -> Unit + val memoryCallback: (Boolean, Int?) -> Unit, ) : ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ConnectivityCompat.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ConnectivityCompat.kt index 9ce9281a1..4a517611a 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ConnectivityCompat.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ConnectivityCompat.kt @@ -24,19 +24,20 @@ internal interface Connectivity { fun hasNetworkConnection(): Boolean fun retrieveNetworkAccessState(): String fun hasAccessNetworkStatePermissionInManifest(context: Context): Boolean { - val packageInfo = context.packageManager.getPackageInfo( context.packageName, - PackageManager.GET_PERMISSIONS + PackageManager.GET_PERMISSIONS, ) val permissions = packageInfo.requestedPermissions - if (permissions.isNullOrEmpty()) + if (permissions.isNullOrEmpty()) { return false + } for (perm in permissions) { - if (perm == Manifest.permission.ACCESS_NETWORK_STATE) + if (perm == Manifest.permission.ACCESS_NETWORK_STATE) { return true + } } return false @@ -45,7 +46,7 @@ internal interface Connectivity { internal class ConnectivityCompat( context: Context, - callback: NetworkChangeCallback? + callback: NetworkChangeCallback?, ) : Connectivity { private val cm = context.getConnectivityManager() @@ -56,7 +57,7 @@ internal class ConnectivityCompat( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> ConnectivityApi24( cm, context, - callback + callback, ) else -> ConnectivityLegacy(context, cm, callback) @@ -85,19 +86,23 @@ internal class ConnectivityCompat( internal class ConnectivityLegacy( private val context: Context, private val cm: ConnectivityManager, - callback: NetworkChangeCallback? + callback: NetworkChangeCallback?, ) : Connectivity { private val changeReceiver = ConnectivityChangeReceiver(callback) private val activeNetworkInfo: android.net.NetworkInfo? @SuppressLint("MissingPermission") - get() = if (hasAccessNetworkStatePermissionInManifest(context)) try { - cm.activeNetworkInfo - } catch (e: NullPointerException) { - // in some rare cases we get a remote NullPointerException via Parcel.readException + get() = if (hasAccessNetworkStatePermissionInManifest(context)) { + try { + cm.activeNetworkInfo + } catch (e: NullPointerException) { + // in some rare cases we get a remote NullPointerException via Parcel.readException + null + } + } else { null - } else null + } override fun registerForNetworkChanges() { val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) @@ -120,7 +125,7 @@ internal class ConnectivityLegacy( } private inner class ConnectivityChangeReceiver( - private val cb: NetworkChangeCallback? + private val cb: NetworkChangeCallback?, ) : BroadcastReceiver() { private val receivedFirstCallback = AtomicBoolean(false) @@ -137,26 +142,31 @@ internal class ConnectivityLegacy( internal class ConnectivityApi24( private val cm: ConnectivityManager, private val context: Context, - callback: NetworkChangeCallback? + callback: NetworkChangeCallback?, ) : Connectivity { private val networkCallback = ConnectivityTrackerCallback(callback) @SuppressLint("MissingPermission") override fun registerForNetworkChanges() { - if (hasAccessNetworkStatePermissionInManifest(context)) + if (hasAccessNetworkStatePermissionInManifest(context)) { cm.registerDefaultNetworkCallback(networkCallback) + } } override fun unregisterForNetworkChanges() = cm.unregisterNetworkCallback(networkCallback) + @SuppressLint("MissingPermission") override fun hasNetworkConnection() = !hasAccessNetworkStatePermissionInManifest(context) || cm.activeNetwork != null @SuppressLint("MissingPermission") override fun retrieveNetworkAccessState(): String { - val network = if (hasAccessNetworkStatePermissionInManifest(context)) - cm.activeNetwork else null + val network = if (hasAccessNetworkStatePermissionInManifest(context)) { + cm.activeNetwork + } else { + null + } val capabilities = if (network != null) cm.getNetworkCapabilities(network) else null return when { @@ -170,7 +180,7 @@ internal class ConnectivityApi24( @VisibleForTesting internal class ConnectivityTrackerCallback( - private val cb: NetworkChangeCallback? + private val cb: NetworkChangeCallback?, ) : ConnectivityManager.NetworkCallback() { private val receivedFirstCallback = AtomicBoolean(false) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ContextExtensions.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ContextExtensions.kt index 57a34fe2f..054b046af 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ContextExtensions.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/ContextExtensions.kt @@ -19,7 +19,7 @@ import java.lang.RuntimeException internal fun Context.registerReceiverSafe( receiver: BroadcastReceiver?, filter: IntentFilter?, - logger: Logger? = null + logger: Logger? = null, ): Intent? { try { return registerReceiver(receiver, filter) @@ -39,7 +39,7 @@ internal fun Context.registerReceiverSafe( */ internal fun Context.unregisterReceiverSafe( receiver: BroadcastReceiver?, - logger: Logger? = null + logger: Logger? = null, ) { try { unregisterReceiver(receiver) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/CustomDateAdapterMoshi.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/CustomDateAdapterMoshi.kt index ce2e1140a..b13586424 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/CustomDateAdapterMoshi.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/CustomDateAdapterMoshi.kt @@ -39,9 +39,8 @@ class CustomDateAdapterMoshi : JsonAdapter() { override fun toJson(writer: JsonWriter, value: Date?) { if (value != null) { synchronized(this) { - writer.value(DateUtils.toIso8601(value) ) + writer.value(DateUtils.toIso8601(value)) } } } - } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DataCollectionModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DataCollectionModule.kt index bedf985ae..4f27e873b 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DataCollectionModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DataCollectionModule.kt @@ -18,7 +18,7 @@ internal class DataCollectionModule( systemServiceModule: SystemServiceModule, bgTaskService: BackgroundTaskService, connectivity: Connectivity, - memoryTrimState: MemoryTrimState + memoryTrimState: MemoryTrimState, ) : DependencyModule() { private val ctx = contextModule.ctx @@ -33,7 +33,7 @@ internal class DataCollectionModule( ctx.packageManager, cfg, systemServiceModule.activityManager, - memoryTrimState + memoryTrimState, ) } @@ -50,7 +50,7 @@ internal class DataCollectionModule( dataDir, rootDetector, bgTaskService, - logger + logger, ) } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultEntityFactory.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultEntityFactory.kt index 855bab06e..cf10bd145 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultEntityFactory.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultEntityFactory.kt @@ -30,4 +30,4 @@ class DefaultEntityFactory : EntityFactory { else -> null } } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultMetrics.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultMetrics.kt index 10d8d6a59..454227b5b 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultMetrics.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultMetrics.kt @@ -20,11 +20,14 @@ import com.rudderstack.android.ruddermetricsreporterandroid.internal.metrics.Def import com.rudderstack.android.ruddermetricsreporterandroid.metrics.AggregatorHandler import com.rudderstack.android.ruddermetricsreporterandroid.metrics.Meter -class DefaultMetrics(private val aggregatorHandler: AggregatorHandler, -private val syncer: Syncer) : Metrics { +class DefaultMetrics( + private val aggregatorHandler: AggregatorHandler, + private val syncer: Syncer, +) : Metrics { override fun getMeter(): Meter { return DefaultMeter(aggregatorHandler) } + @Deprecated("Use [RudderReporter.syncer] instead") override fun getSyncer(): Syncer { return syncer @@ -38,4 +41,4 @@ private val syncer: Syncer) : Metrics { // aggregatorHandler.shutdown() syncer.stopScheduling() } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoir.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoir.kt index a1008db12..e9db170bc 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoir.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoir.kt @@ -34,31 +34,33 @@ import kotlin.math.pow class DefaultReservoir @JvmOverloads constructor( androidContext: Context, useContentProvider: Boolean, - private val dbExecutor: ExecutorService? = null + private val writeKey: String, + private val dbExecutor: ExecutorService? = null, ) : Reservoir { - private val dbName = "metrics_db_${androidContext.packageName}.db" + private val dbName = "metrics_db_${writeKey}_${androidContext.packageName}.db" private val metricDao: Dao private val labelDao: Dao private val errorDao: Dao private var _storageListeners = listOf() - + private val rudderDatabase: RudderDatabase private val maxErrorCount = AtomicLong(MAX_ERROR_COUNT) + init { - RudderDatabase.init( + rudderDatabase= RudderDatabase( androidContext, dbName, DefaultEntityFactory(), useContentProvider, DB_VERSION, - dbExecutor + dbExecutor, ) - metricDao = RudderDatabase.getDao(MetricEntity::class.java) - labelDao = RudderDatabase.getDao(LabelEntity::class.java) - errorDao = RudderDatabase.getDao(ErrorEntity::class.java) + metricDao = rudderDatabase.getDao(MetricEntity::class.java) + labelDao = rudderDatabase.getDao(LabelEntity::class.java) + errorDao = rudderDatabase.getDao(ErrorEntity::class.java) } override fun insertOrIncrement( - metric: MetricModel + metric: MetricModel, ) { val labels = metric.labels.map { LabelEntity(it.key, it.value) } if (labels.isEmpty()) { @@ -68,32 +70,32 @@ class DefaultReservoir @JvmOverloads constructor( with(labelDao) { labels.insertWithDataCallback( - conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE + conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE, ) { rowIds: List, insertedData: List -> if (insertedData.isEmpty()) { insertCounterWithLabelMask(metric, "") return@insertWithDataCallback } - //callback is done inside executor + // callback is done inside executor val insertedIds = getInsertedLabelIds(rowIds, insertedData, labels) - val labelMaskForMetric = if (insertedIds.isEmpty()) "" else run { - val maxIdInserted = insertedIds.max() - val useBigDec = (maxIdInserted >= 63) - if (useBigDec) { - getLabelMaskForMetricWithBigDec(insertedIds) - } else { - getLabelMaskForMetricWithLong(insertedIds) -// .also { -// println("label mask for metric ${metric.name} and labels $insertedIds is $it") -// } + val labelMaskForMetric = if (insertedIds.isEmpty()) { + "" + } else { + run { + val maxIdInserted = insertedIds.max() + val useBigDec = (maxIdInserted >= 63) + if (useBigDec) { + getLabelMaskForMetricWithBigDec(insertedIds) + } else { + getLabelMaskForMetricWithLong(insertedIds) + } } } insertCounterWithLabelMask(metric, labelMaskForMetric) _storageListeners.forEach { it.onDataChange() } } } - } private fun getLabelMaskForMetricWithLong(insertedIds: List): String { @@ -113,7 +115,9 @@ class DefaultReservoir @JvmOverloads constructor( } private fun Dao.getInsertedLabelIds( - rowIds: List, insertedData: List, queryData: List + rowIds: List, + insertedData: List, + queryData: List, ): List { var insertedIds = listOf() rowIds.onEachIndexed { index, rowId -> @@ -123,7 +127,7 @@ class DefaultReservoir @JvmOverloads constructor( val valueOfLabel = queryData[index].value val idOfAlreadyCreatedLabel = runGetQuerySync( selection = "${LabelEntity.Columns.NAME} = ? AND ${LabelEntity.Columns.VALUE} = ?", - selectionArgs = arrayOf(name, valueOfLabel) + selectionArgs = arrayOf(name, valueOfLabel), )?.firstOrNull()?.id if (idOfAlreadyCreatedLabel != null) { insertedIds = insertedIds + idOfAlreadyCreatedLabel @@ -134,23 +138,24 @@ class DefaultReservoir @JvmOverloads constructor( } private fun insertCounterWithLabelMask( - metric: MetricModel, labelMaskForMetric: String + metric: MetricModel, + labelMaskForMetric: String, ) { val metricEntity = MetricEntity( - metric.name, metric.value.toLong(), MetricType.COUNTER.value, labelMaskForMetric + metric.name, + metric.value.toLong(), + MetricType.COUNTER.value, + labelMaskForMetric, ) with(metricDao) { val insertedRowId = listOf(metricEntity).insertSync( - conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE + conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE, )?.firstOrNull() if (insertedRowId == -1L) { -// println("updating metric ${metric.name} label mask $labelMaskForMetric") this.execSqlSync( - "UPDATE " + MetricEntity.TABLE_NAME + " SET " + MetricEntity.ColumnNames.VALUE + " = (" + MetricEntity.ColumnNames.VALUE + " + " + metric.value + ") WHERE " + MetricEntity.ColumnNames.NAME + "='" + metric.name + "'" + " AND " + MetricEntity.ColumnNames.LABEL + "='" + labelMaskForMetric + "'" + " AND " + MetricEntity.ColumnNames.TYPE + "='" + MetricType.COUNTER.value + "'" + ";" + "UPDATE " + MetricEntity.TABLE_NAME + " SET " + MetricEntity.ColumnNames.VALUE + " = (" + MetricEntity.ColumnNames.VALUE + " + " + metric.value + ") WHERE " + MetricEntity.ColumnNames.NAME + "='" + metric.name + "'" + " AND " + MetricEntity.ColumnNames.LABEL + "='" + labelMaskForMetric + "'" + " AND " + MetricEntity.ColumnNames.TYPE + "='" + MetricType.COUNTER.value + "'" + ";", ) - } /*else { - println("inserting metric ${metric.name} label mask $labelMaskForMetric") - }*/ + } } } @@ -160,48 +165,72 @@ class DefaultReservoir @JvmOverloads constructor( return metricEntities?.map { val labels = getLabelsForMetric(it) MetricModelWithId( - it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels + it.id.toString(), + it.name, + MetricType.getType(it.type), + it.value, + labels, ) } ?: listOf() } } override fun getMetricsFirst( - skip: Long, limit: Long, callback: (List>) -> Unit + skip: Long, + limit: Long, + callback: (List>) -> Unit, ) { with(metricDao) { runGetQuery( - limit = limit.toString(), offset = if (skip > 0) skip.toString() else null + limit = limit.toString(), + offset = if (skip > 0) skip.toString() else null, ) { metricEntities -> - callback(metricEntities.map { - val labels = getLabelsForMetric(it) - MetricModelWithId( - it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels - ) - }) + callback( + metricEntities.map { + val labels = getLabelsForMetric(it) + MetricModelWithId( + it.id.toString(), + it.name, + MetricType.getType(it.type), + it.value, + labels, + ) + }, + ) } } } override fun getMetricsFirst( - limit: Long, callback: (List>) -> Unit + limit: Long, + callback: (List>) -> Unit, ) { with(metricDao) { runGetQuery(limit = limit.toString()) { metricEntities -> - callback(metricEntities.map { - val labels = getLabelsForMetric(it) - MetricModelWithId( - it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels - ) - }) + callback( + metricEntities.map { + val labels = getLabelsForMetric(it) + MetricModelWithId( + it.id.toString(), + it.name, + MetricType.getType(it.type), + it.value, + labels, + ) + }, + ) } } } override fun getMetricsAndErrors( - skipForMetrics: Long, skipForErrors: Long, limit: Long, callback: ( - List>, List - ) -> Unit + skipForMetrics: Long, + skipForErrors: Long, + limit: Long, + callback: ( + List>, + List, + ) -> Unit, ) { getMetricsFirst(skipForMetrics, limit) { metrics -> getErrors(skipForErrors, limit) { errors -> @@ -227,7 +256,7 @@ class DefaultReservoir @JvmOverloads constructor( override fun resetMetricsFirst(limit: Long) { with(metricDao) { execSql( - "UPDATE ${MetricEntity.TABLE_NAME} SET ${MetricEntity.ColumnNames.VALUE}=0" + " WHERE ${MetricEntity.ColumnNames.ID} IN (SELECT ${MetricEntity.ColumnNames.ID} " + "FROM ${MetricEntity.TABLE_NAME} ORDER BY ${MetricEntity.ColumnNames.ID} ASC LIMIT $limit)" + "UPDATE ${MetricEntity.TABLE_NAME} SET ${MetricEntity.ColumnNames.VALUE}=0" + " WHERE ${MetricEntity.ColumnNames.ID} IN (SELECT ${MetricEntity.ColumnNames.ID} " + "FROM ${MetricEntity.TABLE_NAME} ORDER BY ${MetricEntity.ColumnNames.ID} ASC LIMIT $limit)", ) } } @@ -244,15 +273,16 @@ class DefaultReservoir @JvmOverloads constructor( synchronized(maxErrorCount) { if (it >= maxErrorCount.get()) return@getCount } - //TODO (add log if exceeded) + // TODO (add log if exceeded) listOf(errorEntity).insert { - //TODO (add log if failed) + // TODO (add log if failed) if (it.isNotEmpty() && it.first() .toLong() > -1 - ) _storageListeners.forEach { it.onDataChange() } + ) { + _storageListeners.forEach { it.onDataChange() } + } } } - } } @@ -262,8 +292,7 @@ class DefaultReservoir @JvmOverloads constructor( override fun getAllErrors(callback: (List) -> Unit) { with(errorDao) { - runGetQuery( - ) { errorEntities -> + runGetQuery() { errorEntities -> callback(errorEntities) } } @@ -280,7 +309,7 @@ class DefaultReservoir @JvmOverloads constructor( runGetQuery( limit = limit.toString(), offset = if (skip > 0) skip.toString() else null, - callback = callback + callback = callback, ) } } @@ -303,9 +332,9 @@ class DefaultReservoir @JvmOverloads constructor( errorDao.delete( whereClause = "${ ErrorEntity.ColumnNames.ID - } IN (${ids.joinToString(",") { it.toString() }})", null + } IN (${ids.joinToString(",") { it.toString() }})", + null, ) - } override fun resetTillSync(dumpedMetrics: List>) { @@ -320,7 +349,7 @@ class DefaultReservoir @JvmOverloads constructor( MetricEntity.ColumnNames.VALUE }-${metric.value.toLong().coerceAtLeast(0L)}) ELSE 0 END " + " WHERE " + "${ MetricEntity.ColumnNames.ID - }='${metric.id}'" + }='${metric.id}'", ) } @@ -340,7 +369,11 @@ class DefaultReservoir @JvmOverloads constructor( metricEntities.map { val labels = getLabelsForMetric(it) MetricModelWithId( - it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels + it.id.toString(), + it.name, + MetricType.getType(it.type), + it.value, + labels, ) }.let { callback(it) @@ -350,13 +383,16 @@ class DefaultReservoir @JvmOverloads constructor( } override fun getAllMetricsSync(): List> { - return with(metricDao) { val metricEntities = getAllSync() metricEntities?.map { val labels = getLabelsForMetric(it) MetricModelWithId( - it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels + it.id.toString(), + it.name, + MetricType.getType(it.type), + it.value, + labels, ) } } ?: listOf() @@ -379,7 +415,6 @@ class DefaultReservoir @JvmOverloads constructor( ++pos labels = labels shr 1 } - } catch (ex: Exception) { var pos = 0 var labels = BigInteger(labelMask) @@ -397,7 +432,7 @@ class DefaultReservoir @JvmOverloads constructor( } with(labelDao) { return runGetQuerySync( - selection = "${LabelEntity.Columns.ID} IN (${labelIds.joinToString(",") { "'${it}'" }})" + selection = "${LabelEntity.Columns.ID} IN (${labelIds.joinToString(",") { "'$it'" }})", )?.associate { it.name to it.value } ?: mapOf() @@ -406,11 +441,11 @@ class DefaultReservoir @JvmOverloads constructor( @VisibleForTesting fun shutDownDatabase() { - RudderDatabase.shutDown() + rudderDatabase.shutDown() } companion object { private const val DB_VERSION = 1 private const val MAX_ERROR_COUNT = 1000L } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt index c542254ad..b148efcca 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt @@ -28,11 +28,15 @@ import java.util.concurrent.atomic.AtomicBoolean class DefaultSyncer internal constructor( private val reservoir: Reservoir, private val uploader: UploadMediator, - private val libraryMetadata: LibraryMetadata + private val libraryMetadata: LibraryMetadata, ) : Syncer { - private var _callback: ((uploadedMetrics: List>, - uploadedErrorModel: ErrorModel, - success: Boolean) -> Unit)? = null + private var _callback: ( + ( + uploadedMetrics: List>, + uploadedErrorModel: ErrorModel, + success: Boolean, + ) -> Unit + )? = null set(value) { synchronized(this) { field = value @@ -45,8 +49,9 @@ class DefaultSyncer internal constructor( private var flushCount = DEFAULT_FLUSH_SIZE private val scheduler = Scheduler() override fun startScheduledSyncs( - interval: Long, flushOnStart: Boolean, - flushCount: Long + interval: Long, + flushOnStart: Boolean, + flushCount: Long, ) { this.flushCount = flushCount _isShutDown.set(false) @@ -64,9 +69,16 @@ class DefaultSyncer internal constructor( * the metrics and errors that were attempted to be uploaded. * */ - override fun setCallback(callback: ((uploadedMetrics: List>, - uploadedErrorModel: ErrorModel, success: - Boolean) -> Unit)?) { + override fun setCallback( + callback: ( + ( + uploadedMetrics: List>, + uploadedErrorModel: ErrorModel, + success: + Boolean, + ) -> Unit + )?, + ) { this._callback = callback } @@ -74,12 +86,13 @@ class DefaultSyncer internal constructor( flush(0L, flushCount) } private fun flush(startIndex: Long, flushCount: Long) { - reservoir.getMetricsAndErrors(startIndex,0, flushCount) { metrics, errors -> + reservoir.getMetricsAndErrors(startIndex, 0, flushCount) { metrics, errors -> val validMetrics = metrics.filterWithValidValues() if (validMetrics.isEmpty() && errors.isEmpty()) { _atomicRunning.set(false) - if (_isShutDown.get()) + if (_isShutDown.get()) { stopScheduling() + } return@getMetricsAndErrors } val errorModel = ErrorModel(libraryMetadata, errors.map { it.errorEvent }) @@ -96,9 +109,9 @@ class DefaultSyncer internal constructor( stopScheduling() return@upload } - if(success) + if (success) { flush(startIndex + flushCount, flushCount) - else + } else _atomicRunning.set(false) } } @@ -106,14 +119,16 @@ class DefaultSyncer internal constructor( override fun stopScheduling() { _isShutDown.set(true) - if (_atomicRunning.get()) + if (_atomicRunning.get()) { return + } scheduler.stop() } override fun flushAllMetrics() { - if (_isShutDown.get()) + if (_isShutDown.get()) { return + } if (_atomicRunning.compareAndSet(false, true)) { flush(flushCount) } @@ -123,7 +138,7 @@ class DefaultSyncer internal constructor( private const val DEFAULT_FLUSH_SIZE = 20L } - class Scheduler internal constructor(){ + class Scheduler internal constructor() { private val thresholdCountDownTimer = Timer("metrics_scheduler") private var periodicTaskScheduler: TimerTask? = null @@ -138,11 +153,10 @@ class DefaultSyncer internal constructor( thresholdCountDownTimer.scheduleAtFixedRate( periodicTaskScheduler, if (callbackOnStart) 0 else flushInterval, - flushInterval + flushInterval, ) - } - fun stop(){ + fun stop() { periodicTaskScheduler?.cancel() thresholdCountDownTimer.cancel() } @@ -152,4 +166,4 @@ class DefaultSyncer internal constructor( it.value.toLong() > 0 } } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploadMediator.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploadMediator.kt index 36b4e8fdb..20c53d5a7 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploadMediator.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploadMediator.kt @@ -29,21 +29,33 @@ internal class DefaultUploadMediator( baseUrl: String, private val jsonAdapter: JsonAdapter, networkExecutor: ExecutorService, - private val apiVersion : Int = 1, - private val isGzipEnabled : Boolean = true + private val apiVersion: Int = 1, + private val isGzipEnabled: Boolean = true, ) : UploadMediator { // private val deviceDataCollector: DeviceDataCollector - private val webService = WebServiceFactory.getWebService(baseUrl, jsonAdapter, - executor = networkExecutor) + private val webService = WebServiceFactory.getWebService( + baseUrl, + jsonAdapter, + executor = networkExecutor, + ) - - override fun upload(metrics: List>, error: ErrorModel, - callback: (success : Boolean) -> Unit) { + override fun upload( + metrics: List>, + error: ErrorModel, + callback: (success: Boolean) -> Unit, + ) { val requestMap = createRequestMap(metrics, error) - webService.post(null,null, jsonAdapter.writeToJson(requestMap, - object: RudderTypeAdapter>() {}), METRICS_ENDPOINT, - object : RudderTypeAdapter>(){}, isGzipEnabled){ - + webService.post( + null, + null, + jsonAdapter.writeToJson( + requestMap, + object : RudderTypeAdapter>() {}, + ), + METRICS_ENDPOINT, + object : RudderTypeAdapter>() {}, + isGzipEnabled, + ) { (it.status in 200..299).apply(callback) } } @@ -58,6 +70,7 @@ internal class DefaultUploadMediator( requestMap[VERSION_KEY] = apiVersion.toString() return requestMap } + // private fun getSourceJsonFromDeviceAndLibrary(deviceJson: String?, // libraryMetadataJson: String?): String? { // return jsonAdapter.writeToJson( @@ -67,7 +80,7 @@ internal class DefaultUploadMediator( // ), object : RudderTypeAdapter>(){} // ) // } - companion object{ + companion object { // private const val DEVICE_KEY = "device" // private const val LIBRARY_METADATA_KEY = "libraryMetadata" private const val SOURCE_KEY = "source" @@ -76,4 +89,4 @@ internal class DefaultUploadMediator( private const val VERSION_KEY = "version" private const val METRICS_ENDPOINT = "sdkmetrics" } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/Device.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/Device.kt index 9fd7dd437..740fbcc6f 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/Device.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/Device.kt @@ -34,8 +34,8 @@ open class Device internal constructor( * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ - runtimeVersions: MutableMap? -) : JSerialize{ + runtimeVersions: MutableMap?, +) : JSerialize { /** * The manufacturer of the device used @@ -63,13 +63,13 @@ open class Device internal constructor( } private fun sanitizeRuntimeVersions(value: MutableMap?): MutableMap = - value?.mapValuesTo(mutableMapOf()) { (_, value) -> value.toString() }?: mutableMapOf() + value?.mapValuesTo(mutableMapOf()) { (_, value) -> value.toString() } ?: mutableMapOf() override fun serialize(jsonAdapter: JsonAdapter): String? { return jsonAdapter.writeToJson(this) } - internal open fun toMap(): Map = mapOf( + internal open fun toMap(): Map = mapOf( "manufacturer" to manufacturer, "model" to model, "osName" to osName, @@ -78,6 +78,6 @@ open class Device internal constructor( "jailbroken" to jailbroken?.toString(), "locale" to locale, "totalMemory" to totalMemory.toString(), - "runtimeVersions" to runtimeVersions.toMap() as? Map + "runtimeVersions" to runtimeVersions.toMap() as? Map, ) } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceBuildInfo.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceBuildInfo.kt index 197b29985..f1294b8b5 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceBuildInfo.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceBuildInfo.kt @@ -11,11 +11,12 @@ internal class DeviceBuildInfo( val fingerprint: String?, val tags: String?, val brand: String?, - val cpuAbis: Array? + val cpuAbis: Array?, ) { companion object { fun defaultInfo(): DeviceBuildInfo { - @Suppress("DEPRECATION") val cpuABis = when { + @Suppress("DEPRECATION") + val cpuABis = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> Build.SUPPORTED_ABIS else -> arrayOf(Build.CPU_ABI, Build.CPU_ABI2) } @@ -29,7 +30,7 @@ internal class DeviceBuildInfo( Build.FINGERPRINT, Build.TAGS, Build.BRAND, - cpuABis + cpuABis, ) } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceDataCollector.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceDataCollector.kt index a0cc4f2c4..9bb8e9469 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceDataCollector.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceDataCollector.kt @@ -32,7 +32,7 @@ internal class DeviceDataCollector( private val dataDirectory: File, rootDetector: RootDetector, private val bgTaskService: BackgroundTaskService, - private val logger: Logger + private val logger: Logger, ) { private val displayMetrics = resources.displayMetrics @@ -58,7 +58,7 @@ internal class DeviceDataCollector( TaskType.IO, Callable { rootDetector.isRooted() - } + }, ) } catch (exc: RejectedExecutionException) { logger.w("Failed to perform root detection checks", exc) @@ -72,7 +72,7 @@ internal class DeviceDataCollector( checkIsRooted(), locale, totalMemoryFuture.runCatching { this?.get() }.getOrNull(), - runtimeVersions.toMutableMap() + runtimeVersions.toMutableMap(), ) fun generateDeviceWithState(now: Long) = DeviceWithState( @@ -84,7 +84,7 @@ internal class DeviceDataCollector( calculateFreeDisk(), calculateFreeMemory(), getOrientationAsString(), - Date(now) + Date(now), ) fun generateInternalDeviceWithState(now: Long) = DeviceWithState( @@ -96,7 +96,7 @@ internal class DeviceDataCollector( calculateFreeDisk(), calculateFreeMemory(), getOrientationAsString(), - Date(now) + Date(now), ) fun getDeviceMetadata(): Map { @@ -198,7 +198,9 @@ internal class DeviceDataCollector( appContext.getLocationManager()?.isLocationEnabled == true else -> { val cr = appContext.contentResolver - @Suppress("DEPRECATION") val providersAllowed = + + @Suppress("DEPRECATION") + val providersAllowed = Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED) providersAllowed != null && providersAllowed.isNotEmpty() } @@ -242,7 +244,7 @@ internal class DeviceDataCollector( return runCatching { bgTaskService.submitTask( TaskType.IO, - Callable { dataDirectory.usableSpace } + Callable { dataDirectory.usableSpace }, ).get() }.getOrDefault(0L) } @@ -276,7 +278,7 @@ internal class DeviceDataCollector( TaskType.DEFAULT, Callable { calculateTotalMemory() - } + }, ) } catch (exc: RejectedExecutionException) { logger.w("Failed to lookup available device memory", exc) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceWithState.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceWithState.kt index e86207a14..bd3806ed4 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceWithState.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DeviceWithState.kt @@ -1,10 +1,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal -import com.fasterxml.jackson.annotation.JsonIgnore -import com.google.gson.annotations.SerializedName -import com.rudderstack.android.ruddermetricsreporterandroid.internal.Device import com.rudderstack.rudderjsonadapter.JsonAdapter -import com.squareup.moshi.Json import java.util.Date /** @@ -36,9 +32,9 @@ class DeviceWithState internal constructor( /** * The timestamp on the device when the event occurred */ - var time: Date?=null, -//private final String timestampString; -) : Device(buildInfo, buildInfo.cpuAbis, jailbroken,locale, totalMemory, runtimeVersions){ + var time: Date? = null, +// private final String timestampString; +) : Device(buildInfo, buildInfo.cpuAbis, jailbroken, locale, totalMemory, runtimeVersions) { override fun serialize(jsonAdapter: JsonAdapter): String? { return jsonAdapter.writeToJson(this) } @@ -48,7 +44,7 @@ class DeviceWithState internal constructor( "freeDisk" to freeDisk.toString(), "freeMemory" to freeMemory.toString(), "orientation" to orientation.toString(), - "time" to time?.let { DateUtils.toIso8601(it)}, + "time" to time?.let { DateUtils.toIso8601(it) }, ) } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/StateEvent.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/StateEvent.kt index 92f0aa740..9e3cca5b4 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/StateEvent.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/StateEvent.kt @@ -9,27 +9,26 @@ sealed class StateEvent { // JvmField allows direct field access optimizations class AddMetadata( @JvmField val section: String, @JvmField val key: String?, - @JvmField val value: Any? + @JvmField val value: Any?, ) : StateEvent() class ClearMetadataSection(@JvmField val section: String) : StateEvent() class ClearMetadataValue( @JvmField val section: String, - @JvmField val key: String? + @JvmField val key: String?, ) : StateEvent() class AddBreadcrumb( @JvmField val message: String, @JvmField val type: BreadcrumbType, @JvmField val timestamp: String, - @JvmField val metadata: MutableMap + @JvmField val metadata: MutableMap, ) : StateEvent() class UpdateMemoryTrimEvent( @JvmField val isLowMemory: Boolean, @JvmField val memoryTrimLevel: Int? = null, - @JvmField val memoryTrimLevelDescription: String = "None" + @JvmField val memoryTrimLevelDescription: String = "None", ) : StateEvent() - } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ConfigModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ConfigModule.kt index 1bd93c924..eb5c900a4 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ConfigModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ConfigModule.kt @@ -9,7 +9,7 @@ import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.sanit */ internal class ConfigModule( contextModule: ContextModule, - configuration: Configuration + configuration: Configuration, ) : DependencyModule() { val config = sanitiseConfiguration(contextModule.ctx, configuration) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ContextModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ContextModule.kt index 7a128471e..41eee9096 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ContextModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/ContextModule.kt @@ -7,7 +7,7 @@ import android.content.Context * context if it is the base context. */ internal class ContextModule( - appContext: Context + appContext: Context, ) : DependencyModule() { val ctx: Context = when (appContext.applicationContext) { diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/DependencyModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/DependencyModule.kt index 6d302f6d8..2a5181eeb 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/DependencyModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/DependencyModule.kt @@ -3,7 +3,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.di import com.rudderstack.android.ruddermetricsreporterandroid.internal.BackgroundTaskService import com.rudderstack.android.ruddermetricsreporterandroid.internal.TaskType - internal abstract class DependencyModule { private val properties = mutableListOf>() @@ -31,7 +30,7 @@ internal abstract class DependencyModule { taskType, Runnable { properties.forEach { it.value } - } + }, ).get() } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/SystemServiceModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/SystemServiceModule.kt index bf6f3657c..1d772d5f5 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/SystemServiceModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/di/SystemServiceModule.kt @@ -1,14 +1,12 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.di import com.rudderstack.android.ruddermetricsreporterandroid.internal.getActivityManager -import com.rudderstack.android.ruddermetricsreporterandroid.internal.getStorageManager - /** * A dependency module which provides a reference to Android system services. */ -internal class SystemServiceModule( - contextModule: ContextModule +internal class SystemServiceModule( + contextModule: ContextModule, ) : DependencyModule() { val activityManager = contextModule.ctx.getActivityManager() diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/BreadcrumbState.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/BreadcrumbState.kt index 76ee6a1d4..d59d62455 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/BreadcrumbState.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/BreadcrumbState.kt @@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger internal class BreadcrumbState( private val maxBreadcrumbs: Int, // private val callbackState: CallbackState, - private val logger: Logger + private val logger: Logger, ) : BaseObservable() { /* @@ -61,7 +61,7 @@ internal class BreadcrumbState( breadcrumb.type, // an encoding of milliseconds since the epoch "t${breadcrumb.timestamp.time}", - breadcrumb.metadata ?: mutableMapOf() + breadcrumb.metadata ?: mutableMapOf(), ) } } @@ -103,6 +103,4 @@ internal class BreadcrumbState( index.set(tail) } } - - } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Error.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Error.kt index a63f8ad43..147577538 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Error.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Error.kt @@ -20,8 +20,8 @@ class Error @JvmOverloads internal constructor( var errorClass: String, var errorMessage: String?, stacktrace: Stacktrace, - var type: ErrorType = ErrorType.ANDROID -){ + var type: ErrorType = ErrorType.ANDROID, +) { internal val stacktrace: List = stacktrace.trace internal companion object { @@ -31,8 +31,11 @@ class Error @JvmOverloads internal constructor( // Somehow it's possible for stackTrace to be null in rare cases val stacktrace = currentEx.stackTrace ?: arrayOf() val trace = Stacktrace(stacktrace, projectPackages, logger) - return@mapTo Error(currentEx.javaClass.name, currentEx.localizedMessage, - trace) + return@mapTo Error( + currentEx.javaClass.name, + currentEx.localizedMessage, + trace, + ) } } } @@ -40,12 +43,12 @@ class Error @JvmOverloads internal constructor( override fun toString(): String { return "Error(errorClass='$errorClass', errorMessage=$errorMessage, stacktrace=$stacktrace, type=$type)" } -internal fun toMap(): Map { + internal fun toMap(): Map { return mapOf( "errorClass" to errorClass, "message" to errorMessage, "stacktrace" to stacktrace.map { it.toMap() }, - "type" to type.toString() + "type" to type.toString(), ) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ErrorType.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ErrorType.kt index ffc5e7f3d..258785d9e 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ErrorType.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ErrorType.kt @@ -18,7 +18,8 @@ enum class ErrorType(internal val desc: String) { /** * An error captured from a Dart / Flutter application */ - DART("dart"); + DART("dart"), + ; internal companion object { @JvmStatic diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ExceptionHandler.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ExceptionHandler.kt index 82b720eb5..8bafbb48d 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ExceptionHandler.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ExceptionHandler.kt @@ -14,19 +14,19 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.error -import com.rudderstack.android.ruddermetricsreporterandroid.error.SeverityReason -import android.os.StrictMode.ThreadPolicy import android.os.StrictMode -import com.rudderstack.android.ruddermetricsreporterandroid.error.DefaultErrorClient +import android.os.StrictMode.ThreadPolicy import com.rudderstack.android.ruddermetricsreporterandroid.Logger +import com.rudderstack.android.ruddermetricsreporterandroid.error.DefaultErrorClient import com.rudderstack.android.ruddermetricsreporterandroid.error.Metadata +import com.rudderstack.android.ruddermetricsreporterandroid.error.SeverityReason /** * Provides automatic notification hooks for unhandled exceptions. */ internal class ExceptionHandler internal constructor( private val errorClient: DefaultErrorClient, - private val logger: Logger + private val logger: Logger, ) : Thread.UncaughtExceptionHandler { private val originalHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler() private val strictModeHandler = StrictModeHandler() @@ -61,13 +61,17 @@ internal class ExceptionHandler internal constructor( StrictMode.setThreadPolicy(ThreadPolicy.LAX) errorClient.notifyUnhandledException( throwable, - metadata, severityReason, violationDesc + metadata, + severityReason, + violationDesc, ) StrictMode.setThreadPolicy(originalThreadPolicy) } else { errorClient.notifyUnhandledException( throwable, - metadata, severityReason, null + metadata, + severityReason, + null, ) } } catch (ignored: Throwable) { @@ -92,4 +96,4 @@ internal class ExceptionHandler internal constructor( private const val STRICT_MODE_TAB = "StrictMode" private const val STRICT_MODE_KEY = "Violation" } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfig.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfig.kt index fe4fc77e8..b89a300ed 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfig.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfig.kt @@ -20,10 +20,10 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import androidx.annotation.VisibleForTesting -import com.rudderstack.android.ruddermetricsreporterandroid.error.BreadcrumbType -import com.rudderstack.android.ruddermetricsreporterandroid.Logger import com.rudderstack.android.ruddermetricsreporterandroid.Configuration import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata +import com.rudderstack.android.ruddermetricsreporterandroid.Logger +import com.rudderstack.android.ruddermetricsreporterandroid.error.BreadcrumbType import com.rudderstack.android.ruddermetricsreporterandroid.error.CrashFilter import com.rudderstack.android.ruddermetricsreporterandroid.internal.DebugLogger import com.rudderstack.android.ruddermetricsreporterandroid.internal.NoopLogger @@ -48,8 +48,8 @@ data class ImmutableConfig( * based on the automatic data capture settings in [Configuration]. */ fun shouldDiscardError(exc: Throwable): Boolean { - return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc) - || shouldDiscardByCrashFilter(exc) + return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc) || + shouldDiscardByCrashFilter(exc) } private fun shouldDiscardByCrashFilter(exc: Throwable): Boolean { @@ -99,13 +99,13 @@ data class ImmutableConfig( } } } + @JvmOverloads internal fun convertToImmutableConfig( config: Configuration, packageInfo: PackageInfo? = null, - appInfo: ApplicationInfo? = null + appInfo: ApplicationInfo? = null, ): ImmutableConfig { - return ImmutableConfig( libraryMetadata = config.libraryMetadata, discardClasses = config.discardClasses.toSet(), @@ -118,12 +118,12 @@ internal fun convertToImmutableConfig( enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), packageInfo = packageInfo, appInfo = appInfo, - crashFilter = config.crashFilter + crashFilter = config.crashFilter, ) } internal fun sanitiseConfiguration( appContext: Context, - configuration: Configuration + configuration: Configuration, ): ImmutableConfig { val packageName = appContext.packageName val packageManager = appContext.packageManager @@ -153,10 +153,16 @@ internal fun sanitiseConfiguration( } if (configuration.libraryMetadata.versionCode.isEmpty() || configuration.libraryMetadata.versionCode == "0") { - configuration.libraryMetadata = configuration.libraryMetadata.copy(versionCode = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - @Suppress("DEPRECATION") - packageInfo?.longVersionCode?.toInt() ?: 0 - } else packageInfo?.versionCode).toString()) + configuration.libraryMetadata = configuration.libraryMetadata.copy( + versionCode = ( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + @Suppress("DEPRECATION") + packageInfo?.longVersionCode?.toInt() ?: 0 + } else { + packageInfo?.versionCode + } + ).toString(), + ) } // Set sensible defaults if project packages not already set @@ -167,10 +173,9 @@ internal fun sanitiseConfiguration( return convertToImmutableConfig( configuration, packageInfo, - appInfo + appInfo, ) } - internal const val RELEASE_STAGE_DEVELOPMENT = "development" internal const val RELEASE_STAGE_PRODUCTION = "production" diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ManifestConfigLoader.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ManifestConfigLoader.kt index c90257297..44f666994 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ManifestConfigLoader.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ManifestConfigLoader.kt @@ -69,8 +69,7 @@ internal class ManifestConfigLoader { */ @VisibleForTesting internal fun load(data: Bundle?): Configuration { - - val config = Configuration(LibraryMetadata("","","", "")) + val config = Configuration(LibraryMetadata("", "", "", "")) if (data != null) { loadAppConfig(config, data) @@ -83,15 +82,15 @@ internal class ManifestConfigLoader { maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads) launchDurationMillis = data.getInt( LAUNCH_CRASH_THRESHOLD_MS, - launchDurationMillis.toInt() + launchDurationMillis.toInt(), ).toLong() launchDurationMillis = data.getInt( LAUNCH_DURATION_MILLIS, - launchDurationMillis.toInt() + launchDurationMillis.toInt(), ).toLong() sendLaunchCrashesSynchronously = data.getBoolean( SEND_LAUNCH_CRASHES_SYNCHRONOUSLY, - sendLaunchCrashesSynchronously + sendLaunchCrashesSynchronously, ) // isAttemptDeliveryOnCrash = data.getBoolean( // ATTEMPT_DELIVERY_ON_CRASH, @@ -123,7 +122,7 @@ internal class ManifestConfigLoader { private fun getStrArray( data: Bundle, key: String, - default: Set? + default: Set?, ): Set? { val delimitedStr = data.getString(key) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MemoryTrimState.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MemoryTrimState.kt index 45bb34f8d..5f09a71f0 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MemoryTrimState.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MemoryTrimState.kt @@ -38,7 +38,7 @@ internal class MemoryTrimState : BaseObservable() { StateEvent.UpdateMemoryTrimEvent( isLowMemory, memoryTrimLevel, - trimLevelDescription + trimLevelDescription, ) } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MetadataState.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MetadataState.kt index 90e3f546c..ce2643ec5 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MetadataState.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/MetadataState.kt @@ -18,8 +18,8 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.Metadata import com.rudderstack.android.ruddermetricsreporterandroid.internal.BaseObservable import com.rudderstack.android.ruddermetricsreporterandroid.internal.StateEvent - -internal data class MetadataState(val metadata: Metadata = Metadata()) : BaseObservable(), +internal data class MetadataState(val metadata: Metadata = Metadata()) : + BaseObservable(), MetadataAware { override fun addMetadata(section: String, value: Map) { @@ -75,7 +75,7 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : BaseObs StateEvent.AddMetadata( section, key, - metadata.getMetadata(section, key) + metadata.getMetadata(section, key), ) } } @@ -87,7 +87,7 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : BaseObs StateEvent.AddMetadata( section, it.key, - metadata.getMetadata(section, it.key) + metadata.getMetadata(section, it.key), ) } } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RootDetector.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RootDetector.kt index 36107550c..283b4ff0e 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RootDetector.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RootDetector.kt @@ -33,7 +33,7 @@ internal class RootDetector @JvmOverloads constructor( private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo(), private val rootBinaryLocations: List = ROOT_INDICATORS, private val buildProps: File = BUILD_PROP_FILE, - private val logger: Logger + private val logger: Logger, ) { companion object { @@ -52,7 +52,7 @@ internal class RootDetector @JvmOverloads constructor( // Fallback "/system/xbin/daemonsu", // Systemless root - "/su/bin" + "/su/bin", ) } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RudderErrorStateModule.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RudderErrorStateModule.kt index bac35dcd2..1607d6e1e 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RudderErrorStateModule.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/RudderErrorStateModule.kt @@ -16,7 +16,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.error import com.rudderstack.android.ruddermetricsreporterandroid.Configuration import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.DependencyModule -import java.util.concurrent.ConcurrentHashMap /** * A dependency module which constructs the objects that track state in Bugsnag. For example, this @@ -24,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap */ internal class RudderErrorStateModule( cfg: ImmutableConfig, - configuration: Configuration + configuration: Configuration, ) : DependencyModule() { val breadcrumbState = BreadcrumbState(cfg.maxBreadcrumbs, cfg.logger) diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Severity.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Severity.kt index a6617705a..b16d28ffe 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Severity.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Severity.kt @@ -1,7 +1,5 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.error -import java.io.IOException - /** * The severity of an Event, one of "error", "warning" or "info". * @@ -11,7 +9,7 @@ import java.io.IOException enum class Severity(private val str: String) { ERROR("error"), WARNING("warning"), - INFO("info"); + INFO("info"), // internal companion object { // internal fun fromDescriptor(desc: String) = values().find { it.str == desc } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stackframe.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stackframe.kt index 102f066ac..0fc33a97c 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stackframe.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stackframe.kt @@ -17,7 +17,7 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.error /** * Represents a single stackframe from a [Throwable] */ -class Stackframe{ +class Stackframe { /** * The name of the method that was being executed @@ -88,7 +88,7 @@ class Stackframe{ lineNumber: Number?, inProject: Boolean?, code: Map? = null, - columnNumber: Number? = null + columnNumber: Number? = null, ) { this.method = method this.file = file diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stacktrace.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stacktrace.kt index ce1e3703c..0f28ee57f 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stacktrace.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/Stacktrace.kt @@ -43,7 +43,7 @@ internal class Stacktrace { val trace: List - private constructor(){ + private constructor() { trace = emptyList() } constructor(frames: List) { @@ -53,7 +53,7 @@ internal class Stacktrace { constructor( stacktrace: Array, projectPackages: Collection, - logger: Logger + logger: Logger, ) { val frames = limitTraceLength(stacktrace) trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) } @@ -76,7 +76,7 @@ internal class Stacktrace { private fun serializeStackframe( el: StackTraceElement, projectPackages: Collection, - logger: Logger + logger: Logger, ): Stackframe? { try { val className = el.className @@ -89,7 +89,7 @@ internal class Stacktrace { methodName, el.fileName ?: "Unknown", el.lineNumber, - inProject(className, projectPackages) + inProject(className, projectPackages), ) } catch (lineEx: Exception) { logger.w("Failed to serialize stacktrace", lineEx) @@ -100,5 +100,4 @@ internal class Stacktrace { override fun toString(): String { return "Stacktrace{trace=$trace}" } - } diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ThrowableExtensions.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ThrowableExtensions.kt index e7a417dc3..33831c52f 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ThrowableExtensions.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ThrowableExtensions.kt @@ -1,4 +1,5 @@ @file:JvmName("ThrowableUtils") + package com.rudderstack.android.ruddermetricsreporterandroid.internal.error /** diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/Constants.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/Constants.kt index 1de515047..295525489 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/Constants.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/Constants.kt @@ -14,7 +14,7 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.metrics -object LABEL_CONSTANTS{ +object LABEL_CONSTANTS { const val LABEL_TRACK = 0X1 const val LABEL_IDENTIFY = 0X2 const val LABEL_ALIAS = 0X4 @@ -28,4 +28,4 @@ object LABEL_CONSTANTS{ const val LABEL_bla6 = 0X400 const val LABEL_bla7 = 0X800 const val LABEL_bla8 = 0X1000 -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultAggregatorHandler.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultAggregatorHandler.kt index 48e62ef1d..c23c7624d 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultAggregatorHandler.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultAggregatorHandler.kt @@ -22,32 +22,46 @@ import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType import java.util.concurrent.atomic.AtomicBoolean -class DefaultAggregatorHandler(private val reservoir: Reservoir, -isEnabled: Boolean = true) : AggregatorHandler { +class DefaultAggregatorHandler( + private val reservoir: Reservoir, + isEnabled: Boolean = true, +) : AggregatorHandler { private val _isEnabled = AtomicBoolean(isEnabled) override fun LongCounter.recordMetric(value: Long) { - if(!_isEnabled.get()) return + if (!_isEnabled.get()) return recordMetric(value, mapOf()) } - override fun LongCounter.recordMetric(value: Long, attributes: Map) { - if(!_isEnabled.get()) return - reservoir.insertOrIncrement(MetricModel(name, MetricType.COUNTER, - value, attributes)) + override fun LongCounter.recordMetric(value: Long, attributes: Map) { + if (!_isEnabled.get()) return + reservoir.insertOrIncrement( + MetricModel( + name, + MetricType.COUNTER, + value, + attributes, + ), + ) } override fun LongGauge.recordMetric(value: Long) { - if(!_isEnabled.get()) return + if (!_isEnabled.get()) return recordMetric(value, mapOf()) } - override fun LongGauge.recordMetric(value: Long, attributes: Map) { - if(!_isEnabled.get()) return - reservoir.insertOrIncrement(MetricModel(name, MetricType.GAUGE, - value, attributes)) + override fun LongGauge.recordMetric(value: Long, attributes: Map) { + if (!_isEnabled.get()) return + reservoir.insertOrIncrement( + MetricModel( + name, + MetricType.GAUGE, + value, + attributes, + ), + ) } override fun enable(enable: Boolean) { _isEnabled.set(enable) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultMeter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultMeter.kt index cc1b3021b..ff860576d 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultMeter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/DefaultMeter.kt @@ -16,13 +16,10 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal.metrics import com.rudderstack.android.ruddermetricsreporterandroid.metrics.AggregatorHandler import com.rudderstack.android.ruddermetricsreporterandroid.metrics.LongCounter -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.LongGauge import com.rudderstack.android.ruddermetricsreporterandroid.metrics.Meter -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.ShortGauge class DefaultMeter(private val aggregatorHandler: AggregatorHandler) : Meter { override fun longCounter(name: String): LongCounter { return LongCounter(name, aggregatorHandler) } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/RestModelAdapter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/RestModelAdapter.kt index c6553449a..cc12bb033 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/RestModelAdapter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/metrics/RestModelAdapter.kt @@ -21,9 +21,8 @@ import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType * */ internal interface RestModelAdapter { - val name:String - val type:MetricType - val value:Long + val name: String + val type: MetricType + val value: Long val labels: Map - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorHandler.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorHandler.kt index c1e7f5699..708ce8969 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorHandler.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorHandler.kt @@ -16,9 +16,9 @@ package com.rudderstack.android.ruddermetricsreporterandroid.metrics interface AggregatorHandler { fun LongCounter.recordMetric(value: Long) - fun LongCounter.recordMetric(value: Long, attributes: Map) + fun LongCounter.recordMetric(value: Long, attributes: Map) fun LongGauge.recordMetric(value: Long) - fun LongGauge.recordMetric(value: Long, attributes: Map) + fun LongGauge.recordMetric(value: Long, attributes: Map) - fun enable(enable : Boolean) -} \ No newline at end of file + fun enable(enable: Boolean) +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorTemporality.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorTemporality.kt index 54430158e..cd913af6a 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorTemporality.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/AggregatorTemporality.kt @@ -20,6 +20,7 @@ package com.rudderstack.android.ruddermetricsreporterandroid.metrics enum class AggregatorTemporality { /** Measurements are aggregated since the previous collection. */ DELTA, + /** Measurements are aggregated over the lifetime of the instrument. */ - CUMULATIVE -} \ No newline at end of file + CUMULATIVE, +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Counter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Counter.kt index 8fba7e3fa..d70c11cd5 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Counter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Counter.kt @@ -31,8 +31,7 @@ sealed interface Counter { * @param value The increment amount. MUST be non-negative. * @param attributes A set of attributes to associate with the value. */ - fun add(value: T, attributes: Map) - + fun add(value: T, attributes: Map) } class LongCounter internal constructor( @@ -46,10 +45,9 @@ class LongCounter internal constructor( } } - override fun add(value: Long, attributes: Map) { + override fun add(value: Long, attributes: Map) { with(aggregatorHandler) { recordMetric(value, attributes) } } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Gauge.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Gauge.kt index 7db7033db..da25bf6c0 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Gauge.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Gauge.kt @@ -18,6 +18,7 @@ import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir sealed interface Gauge { val name: String + /** * Records a value. * @@ -32,28 +33,27 @@ sealed interface Gauge { * @param value The amount for gauge * @param attributes A set of attributes to associate with the value. */ - fun set(value: T, attributes: Map?) + fun set(value: T, attributes: Map?) } class LongGauge internal constructor( override val name: String, _aggregatorHandle: AggregatorHandler, - _reservoir: Reservoir + _reservoir: Reservoir, ) : Gauge { override fun set(value: Long) { TODO("Not yet implemented") } - override fun set(value: Long, attributes: Map?) { + override fun set(value: Long, attributes: Map?) { TODO("Not yet implemented") } - } class ShortGauge internal constructor( override val name: String, _aggregatorHandle: AggregatorHandler, - _reservoir: Reservoir + _reservoir: Reservoir, ) : Gauge { private val aggregatorHandler = _aggregatorHandle private val reservoir = _reservoir @@ -61,7 +61,7 @@ class ShortGauge internal constructor( TODO("Not yet implemented") } - override fun set(value: Short, attributes: Map?) { + override fun set(value: Short, attributes: Map?) { TODO("Not yet implemented") } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Meter.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Meter.kt index 84b7eec0a..8338dbd3a 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Meter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/Meter.kt @@ -16,7 +16,7 @@ package com.rudderstack.android.ruddermetricsreporterandroid.metrics interface Meter { fun longCounter(name: String): LongCounter - //we will not be supporting gauges for now + // we will not be supporting gauges for now // fun longGauge(name: String): LongGauge // fun shortGauge(name: String?): ShortGauge -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricModel.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricModel.kt index d880c4026..e8ed93d73 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricModel.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricModel.kt @@ -21,7 +21,10 @@ import com.rudderstack.rudderjsonadapter.JsonAdapter import com.squareup.moshi.Json open class MetricModel( - val name: String, val type: MetricType, val value: T, val labels: Map + val name: String, + val type: MetricType, + val value: T, + val labels: Map, ) : JSerialize> { companion object { @@ -71,8 +74,6 @@ open class MetricModel( override fun toString(): String { return "MetricModel($NAME_TAG ='$name', $TYPE_TAG = $type, $VALUE_TAG = $value, $LABELS_TAG = $labels)" } - - } class MetricModelWithId( @@ -80,7 +81,7 @@ class MetricModelWithId( name: String, type: MetricType, value: T, - labels: Map + labels: Map, ) : MetricModel(name, type, value, labels) { companion object { @Keep private const val ID_TAG = "id" @@ -101,5 +102,4 @@ class MetricModelWithId( override fun toString(): String { return "MetricModelWithId(i$ID_TAG='$id'), parent = ${super.toString()})" } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricType.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricType.kt index 46f1f5ce6..3eb73f51e 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricType.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/metrics/MetricType.kt @@ -28,7 +28,8 @@ enum class MetricType(val value: String) { @SerializedName("gauge") @JsonProperty("gauge") @Json(name = "gauge") - GAUGE("gauge"); + GAUGE("gauge"), + ; companion object { fun getType(type: String): MetricType { @@ -39,4 +40,4 @@ enum class MetricType(val value: String) { } } } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/ErrorEntity.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/ErrorEntity.kt index b6e966b97..7ecad601d 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/ErrorEntity.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/ErrorEntity.kt @@ -18,21 +18,27 @@ import android.content.ContentValues import com.rudderstack.android.repository.Entity import com.rudderstack.android.repository.annotation.RudderEntity import com.rudderstack.android.repository.annotation.RudderField -import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorEvent @RudderEntity( - tableName = ErrorEntity.TABLE_NAME, [ + tableName = ErrorEntity.TABLE_NAME, + [ RudderField( - RudderField.Type.INTEGER, ErrorEntity.ColumnNames.ID, - primaryKey = true, isNullable = false, isAutoInc = true, isIndex = true + RudderField.Type.INTEGER, + ErrorEntity.ColumnNames.ID, + primaryKey = true, + isNullable = false, + isAutoInc = true, + isIndex = true, ), RudderField( - RudderField.Type.TEXT, ErrorEntity.ColumnNames.ERROR_EVENT, - primaryKey = false, isNullable = false - ) - ] + RudderField.Type.TEXT, + ErrorEntity.ColumnNames.ERROR_EVENT, + primaryKey = false, + isNullable = false, + ), + ], ) -class ErrorEntity(val errorEvent: String): Entity { +class ErrorEntity(val errorEvent: String) : Entity { private var _id: Long = UNINITIALIZED_ID val id: Long @@ -51,7 +57,7 @@ class ErrorEntity(val errorEvent: String): Entity { val errorEvent = values[ColumnNames.ERROR_EVENT] as String val id = values[ColumnNames.ID] as? Long return ErrorEntity(errorEvent).also { - if(id != null) { + if (id != null) { it._id = id } } @@ -69,11 +75,11 @@ class ErrorEntity(val errorEvent: String): Entity { } override fun equals(other: Any?): Boolean { - return other is ErrorEntity && other.id == id - && other.errorEvent == errorEvent + return other is ErrorEntity && other.id == id && + other.errorEvent == errorEvent } override fun hashCode(): Int { return id.hashCode() + errorEvent.hashCode() } -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/LabelEntity.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/LabelEntity.kt index 3658b9a3f..6353f9ae7 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/LabelEntity.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/LabelEntity.kt @@ -20,30 +20,31 @@ import com.rudderstack.android.repository.annotation.RudderEntity import com.rudderstack.android.repository.annotation.RudderField @RudderEntity( - LabelEntity.TABLE_NAME, [ + LabelEntity.TABLE_NAME, + [ RudderField( RudderField.Type.INTEGER, LabelEntity.Columns.ID, primaryKey = false, isNullable = false, isAutoInc = true, - isIndex = true + isIndex = true, ), RudderField( RudderField.Type.TEXT, LabelEntity.Columns.NAME, primaryKey = true, isNullable = false, - isUnique = true + isUnique = true, ), RudderField( RudderField.Type.TEXT, LabelEntity.Columns.VALUE, primaryKey = true, isNullable = false, - isUnique = true - ) - ] + isUnique = true, + ), + ], ) internal class LabelEntity(val name: String, val value: String) : Entity { private var _id: Long = UNINITIALIZED_ID @@ -80,5 +81,4 @@ internal class LabelEntity(val name: String, val value: String) : Entity { } } } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/MetricEntity.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/MetricEntity.kt index 33024d4a5..4a2d96494 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/MetricEntity.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/MetricEntity.kt @@ -26,34 +26,48 @@ import com.rudderstack.android.ruddermetricsreporterandroid.models.MetricEntity. * */ @RudderEntity( - tableName = TABLE_NAME, [ + tableName = TABLE_NAME, + [ RudderField( - RudderField.Type.INTEGER, MetricEntity.ColumnNames.ID, - primaryKey = false, isNullable = false, isAutoInc = true, isIndex = true + RudderField.Type.INTEGER, + MetricEntity.ColumnNames.ID, + primaryKey = false, + isNullable = false, + isAutoInc = true, + isIndex = true, ), RudderField( - RudderField.Type.TEXT, MetricEntity.ColumnNames.NAME, - primaryKey = true, isNullable = false + RudderField.Type.TEXT, + MetricEntity.ColumnNames.NAME, + primaryKey = true, + isNullable = false, ), RudderField( - RudderField.Type.INTEGER, MetricEntity.ColumnNames.VALUE, - primaryKey = false, isNullable = false + RudderField.Type.INTEGER, + MetricEntity.ColumnNames.VALUE, + primaryKey = false, + isNullable = false, ), RudderField( - RudderField.Type.TEXT, MetricEntity.ColumnNames.TYPE, - primaryKey = true, isNullable = false + RudderField.Type.TEXT, + MetricEntity.ColumnNames.TYPE, + primaryKey = true, + isNullable = false, ), RudderField( - RudderField.Type.TEXT, MetricEntity.ColumnNames.LABEL, - primaryKey = true, isNullable = false, isIndex = true - ) - ] + RudderField.Type.TEXT, + MetricEntity.ColumnNames.LABEL, + primaryKey = true, + isNullable = false, + isIndex = true, + ), + ], ) internal class MetricEntity( val name: String, val value: Long, val type: String, - val label: String + val label: String, ) : Entity { object ColumnNames { const val ID = "id" @@ -95,8 +109,6 @@ internal class MetricEntity( return MetricEntity(name, value, type, label).also { it._id = id } - } } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/Constants.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/Constants.kt index 279b52ea3..6728500de 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/Constants.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/Constants.kt @@ -270,4 +270,4 @@ const val TEST_ERROR_EVENTS_JSON = } } -""" \ No newline at end of file +""" diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleUnitTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleUnitTest.kt index 887c8f96f..ae8a06d02 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleUnitTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package com.rudderstack.android.ruddermetricsreporterandroid -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test import kotlin.math.ln /** @@ -17,4 +16,4 @@ class ExampleUnitTest { val log8base2 = (ln(8.0) / ln(2.0)).toInt() assert(log8base2 == 3) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt index a13b72acb..1a4ba8d47 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt @@ -15,34 +15,38 @@ package com.rudderstack.android.ruddermetricsreporterandroid import android.os.Build -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploaderTestGson -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploaderTestJackson -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploaderTestMoshi import com.rudderstack.gsonrudderadapter.GsonAdapter import com.rudderstack.jacksonrudderadapter.JacksonAdapter import com.rudderstack.moshirudderadapter.MoshiAdapter import com.rudderstack.rudderjsonadapter.JsonAdapter import org.junit.Assert.* - import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite abstract class LibraryMetadataTest { protected abstract val jsonAdapter: JsonAdapter + @Test fun serialize() { - val libraryMetadata = LibraryMetadata("test","1.0","4","abcde") + val libraryMetadata = LibraryMetadata("test", "1.0", "4", "abcde") val json = libraryMetadata.serialize(jsonAdapter) - assertEquals("{\"name\":\"test\",\"sdk_version\":\"1.0\",\"version_code\":\"4\"," + - "\"write_key\":\"abcde\",\"os_version\":\"${Build.VERSION.SDK_INT}\"}",json) + assertEquals( + "{\"name\":\"test\",\"sdk_version\":\"1.0\",\"version_code\":\"4\"," + + "\"write_key\":\"abcde\",\"os_version\":\"${Build.VERSION.SDK_INT}\"}", + json, + ) } + @Test fun `serialize with version`() { - val libraryMetadata = LibraryMetadata("test","1.0","4","abcde", "[14]") + val libraryMetadata = LibraryMetadata("test", "1.0", "4", "abcde", "[14]") val json = libraryMetadata.serialize(jsonAdapter) - assertEquals("{\"name\":\"test\",\"sdk_version\":\"1.0\",\"version_code\":\"4\"," + - "\"write_key\":\"abcde\",\"os_version\":\"[14]\"}",json) + assertEquals( + "{\"name\":\"test\",\"sdk_version\":\"1.0\",\"version_code\":\"4\"," + + "\"write_key\":\"abcde\",\"os_version\":\"[14]\"}", + json, + ) } } class LibraryMetadataTestGson : LibraryMetadataTest() { @@ -54,11 +58,11 @@ class LibraryMetadataTestJackson : LibraryMetadataTest() { class LibraryMetadataTestMoshi : LibraryMetadataTest() { override val jsonAdapter: JsonAdapter = MoshiAdapter() } + @RunWith(Suite::class) @Suite.SuiteClasses( LibraryMetadataTestGson::class, LibraryMetadataTestJackson::class, - LibraryMetadataTestMoshi::class + LibraryMetadataTestMoshi::class, ) -class DefaultMetadataTestSuite { -} \ No newline at end of file +class DefaultMetadataTestSuite diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbTest.kt index 0de55f4e6..665c74b3d 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/BreadcrumbTest.kt @@ -14,18 +14,14 @@ package com.rudderstack.android.ruddermetricsreporterandroid.error -import com.rudderstack.android.ruddermetricsreporterandroid.TEST_ERROR_EVENTS_JSON import com.rudderstack.android.ruddermetricsreporterandroid.internal.NoopLogger import com.rudderstack.gsonrudderadapter.GsonAdapter import com.rudderstack.jacksonrudderadapter.JacksonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.equalTo import org.junit.Test import java.util.Calendar -import java.util.Date -import java.util.Locale import java.util.TimeZone class BreadcrumbTest { @@ -44,7 +40,7 @@ class BreadcrumbTest { private val jacksonAdapter = JacksonAdapter() @Test - fun `Breadcrumb serialization test`(){ + fun `Breadcrumb serialization test`() { val breadcrumb = Breadcrumb( "test", BreadcrumbType.ERROR, @@ -58,29 +54,33 @@ class BreadcrumbTest { set(Calendar.SECOND, 32) set(Calendar.MILLISECOND, 0) }.time, - NoopLogger + NoopLogger, ) val breadcrumbJson = gsonAdapter.writeToJson(breadcrumb) val breadcrumbJsonJackson = jacksonAdapter.writeToJson(breadcrumb) val gsonExpected = gsonAdapter.readJson( TEST_BREADCRUMB_JSON, object : RudderTypeAdapter>() { - }) + }, + ) val jacksonExpected = jacksonAdapter.readJson( TEST_BREADCRUMB_JSON, object : RudderTypeAdapter>() { - }) + }, + ) val gsonActual = gsonAdapter.readJson( breadcrumbJson!!, object : RudderTypeAdapter>() { - }) + }, + ) val jacksonActual = jacksonAdapter.readJson( breadcrumbJson!!, object : RudderTypeAdapter>() { - }) - assertThat(breadcrumbJson, equalTo( breadcrumbJsonJackson)) + }, + ) + assertThat(breadcrumbJson, equalTo(breadcrumbJsonJackson)) assertThat(gsonActual, equalTo(gsonExpected)) assertThat(jacksonActual, equalTo(jacksonExpected)) assertThat(gsonActual, equalTo(jacksonActual)) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilterTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilterTest.kt index 0997bde43..7af0e23b8 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilterTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/CrashFilterTest.kt @@ -51,5 +51,4 @@ class CrashFilterTest { val exception = Exception("some_exception", Exception("some_other_exception")) assert(!crashFilter.shouldKeep(exception)) } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEventTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEventTest.kt index 9b9b2cdf4..124055411 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEventTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorEventTest.kt @@ -17,44 +17,35 @@ package com.rudderstack.android.ruddermetricsreporterandroid.error import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata -import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadataTestGson -import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadataTestJackson -import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadataTestMoshi import com.rudderstack.android.ruddermetricsreporterandroid.Logger import com.rudderstack.android.ruddermetricsreporterandroid.TEST_ERROR_EVENTS_JSON import com.rudderstack.android.ruddermetricsreporterandroid.internal.AppWithState -import com.rudderstack.android.ruddermetricsreporterandroid.internal.CustomDateAdapterMoshi import com.rudderstack.android.ruddermetricsreporterandroid.internal.DeviceBuildInfo import com.rudderstack.android.ruddermetricsreporterandroid.internal.DeviceWithState import com.rudderstack.android.ruddermetricsreporterandroid.internal.error.ImmutableConfig import com.rudderstack.gsonrudderadapter.GsonAdapter import com.rudderstack.jacksonrudderadapter.JacksonAdapter -import com.rudderstack.moshirudderadapter.MoshiAdapter import com.rudderstack.rudderjsonadapter.JsonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import org.hamcrest.MatcherAssert import org.hamcrest.Matchers.contains -import org.hamcrest.Matchers.equalTo import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite import java.util.Calendar -import java.util.Date -import java.util.Locale import java.util.TimeZone abstract class ErrorEventTest { abstract val jsonAdapter: JsonAdapter - @Test fun serialize() { val immutableConfig = ImmutableConfig( LibraryMetadata( "test_lib", - "1.3.0", "14", "my_write_key" + "1.3.0", + "14", + "my_write_key", ), listOf("com.rudderstack.android"), setOf(BreadcrumbType.ERROR), @@ -68,26 +59,36 @@ abstract class ErrorEventTest { PackageInfo().also { it.packageName = "com.example.myPackage" }, - ApplicationInfo() + ApplicationInfo(), ) val errorEvent = ErrorEvent( - originalError = Exception(), //if this line moves from 69, change the line number in + originalError = Exception(), // if this line moves from 69, change the line number in // testErrorEventJson line 46 file=ErrorEventTest.kt lineNumber=309.0, config = immutableConfig, severityReason = SeverityReason.newInstance(SeverityReason.REASON_ANR), - data = Metadata(store = mutableMapOf("m1" to mutableMapOf("dumb" to "dumber"))) + data = Metadata(store = mutableMapOf("m1" to mutableMapOf("dumb" to "dumber"))), + ) + errorEvent.app = AppWithState( + immutableConfig, + "arm64", + "write_key", + "release", + "2.1.0", + "reporter.test", ) - errorEvent.app = AppWithState(immutableConfig, "arm64", "write_key", "release", "2.1.0", - "reporter.test") errorEvent.device = DeviceWithState( DeviceBuildInfo( "LG", "Nexus", "8.0.1", 29, null, "null", null, "LG", arrayOf("x86_64"), - ), false, "locale", 1234556L, mutableMapOf( + ), + false, "locale", 1234556L, + mutableMapOf( "androidApiLevel" to "29", - "osBuild" to "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" - ), 54354354L, - 45345345L, null, Calendar.getInstance(TimeZone.getTimeZone("US")).apply { + "osBuild" to "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys", + ), + 54354354L, + 45345345L, null, + Calendar.getInstance(TimeZone.getTimeZone("US")).apply { set(Calendar.YEAR, 2023) set(Calendar.MONTH, 7) set(Calendar.DAY_OF_MONTH, 31) @@ -95,25 +96,34 @@ abstract class ErrorEventTest { set(Calendar.MINUTE, 32) set(Calendar.SECOND, 32) set(Calendar.MILLISECOND, 0) - }.time + }.time, ) - val actual = jsonAdapter.readJson(errorEvent.serialize(jsonAdapter)!!.also { println(it) }, + val actual = jsonAdapter.readJson( + errorEvent.serialize(jsonAdapter)!!.also { println(it) }, object : RudderTypeAdapter>() { - - }) - val expected = jsonAdapter.readJson(TEST_ERROR_EVENTS_JSON, + }, + ) + val expected = jsonAdapter.readJson( + TEST_ERROR_EVENTS_JSON, object : RudderTypeAdapter>() { - }) + }, + ) - MatcherAssert.assertThat(actual?.entries?.filter { - it.key != "exceptions" }?.also { println("parser: ${jsonAdapter::class.simpleName}") - println(it) - }, - contains - (*(expected!!.entries!!.filter { it.key != "exceptions" }.toTypedArray().also { - println("expected for parser: ${jsonAdapter::class.simpleName}") + MatcherAssert.assertThat( + actual?.entries?.filter { + it.key != "exceptions" + }?.also { + println("parser: ${jsonAdapter::class.simpleName}") println(it) - })) + }, + contains( + *( + expected!!.entries!!.filter { it.key != "exceptions" }.toTypedArray().also { + println("expected for parser: ${jsonAdapter::class.simpleName}") + println(it) + } + ), + ), ) } } @@ -121,14 +131,12 @@ abstract class ErrorEventTest { class GsonErrorEventTest : ErrorEventTest() { override val jsonAdapter: JsonAdapter get() = GsonAdapter() - } class JacksonErrorEventTest : ErrorEventTest() { override val jsonAdapter: JsonAdapter get() = JacksonAdapter() - } -//class MoshiErrorEventTest : ErrorEventTest() { +// class MoshiErrorEventTest : ErrorEventTest() { // override val jsonAdapter: JsonAdapter // get() = MoshiAdapter( Moshi.Builder() // .add(CustomDateAdapterMoshi()) @@ -136,7 +144,7 @@ class JacksonErrorEventTest : ErrorEventTest() { // .addLast(KotlinJsonAdapterFactory()) // .build()) // -//} +// } @RunWith(Suite::class) @Suite.SuiteClasses( @@ -144,5 +152,4 @@ class JacksonErrorEventTest : ErrorEventTest() { JacksonErrorEventTest::class, // MoshiErrorEventTest::class ) -class DefaultErrorEventsTestSuite { -} \ No newline at end of file +class DefaultErrorEventsTestSuite diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModelTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModelTest.kt index 3904174f3..4599957de 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModelTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/error/ErrorModelTest.kt @@ -19,7 +19,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.gsonrudderadapter.GsonAdapter import com.rudderstack.jacksonrudderadapter.JacksonAdapter import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers import org.hamcrest.Matchers.emptyString import org.hamcrest.Matchers.not import org.junit.Test @@ -37,21 +36,24 @@ class ErrorModelTest { """.trimIndent(), """ {"exceptions":[{"errorClass":"java.lang.NullPointerException","stacktrace":[{"method":"com.google.gson.internal.${"\$"}Gson${"\$"}Preconditions.checkNotNull","file":"${"\$"}Gson${"\$"}Preconditions.java","lineNumber":39},{"method":"com.google.gson.reflect.TypeToken.\u003cinit\u003e","file":"TypeToken.java","lineNumber":72},{"method":"com.google.gson.reflect.TypeToken.get","file":"TypeToken.java","lineNumber":296},{"method":"com.google.gson.Gson.fromJson","file":"Gson.java","lineNumber":961},{"method":"com.google.gson.Gson.fromJson","file":"Gson.java","lineNumber":928},{"method":"com.google.gson.Gson.fromJson","file":"Gson.java","lineNumber":877},{"method":"com.rudderstack.gsonrudderadapter.GsonAdapter.readJson","file":"GsonAdapter.kt","lineNumber":31},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel.toMap","file":"ErrorModel.kt","lineNumber":33},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploadMediator.createRequestMap","file":"DefaultUploadMediator.kt","lineNumber":59},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploadMediator.upload","file":"DefaultUploadMediator.kt","lineNumber":44},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultSyncer${"\$"}flush${"\$"}{'${"\$"}'}1.invoke","file":"DefaultSyncer.kt","lineNumber":86},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultSyncer${"\$"}flush${"\$"}{'${"\$"}'}1.invoke","file":"DefaultSyncer.kt","lineNumber":77},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir${"\$"}getMetricsAndErrors${"\$"}{'${"\$"}'}1${"\$"}{'${"\$"}'}1.invoke","file":"DefaultReservoir.kt","lineNumber":207},{"method":"com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir${"\$"}getMetricsAndErrors${"\$"}{'${"\$"}'}1${"\$"}{'${"\$"}'}1.invoke","file":"DefaultReservoir.kt","lineNumber":206},{"method":"com.rudderstack.android.repository.Dao${"\$"}runGetQuery${"\$"}{'${"\$"}'}1.invoke","file":"Dao.kt","lineNumber":281},{"method":"com.rudderstack.android.repository.Dao${"\$"}runGetQuery${"\$"}{'${"\$"}'}1.invoke","file":"Dao.kt","lineNumber":280},{"method":"com.rudderstack.android.repository.Dao.runTransactionOrDeferToCreation${"\$"}lambda${"\$"}{'${"\$"}'}26${"\$"}lambda${"\$"}{'${"\$"}'}25","file":"Dao.kt","lineNumber":533},{"method":"com.rudderstack.android.repository.Dao.${"\$"}r8${"\$"}lambda${"\$"}p8FNe_rvlOF8LAj8QetoDZD7f2U","file":"Dao.kt","lineNumber":0},{"method":"com.rudderstack.android.repository.Dao${"\$"}{'${"\$"}'}${"\$"}ExternalSyntheticLambda1.run","file":"R8${"\$"}{'${"\$"}'}${"\$"}SyntheticClass","lineNumber":0},{"method":"java.util.concurrent.ThreadPoolExecutor.runWorker","file":"ThreadPoolExecutor.java","lineNumber":1167},{"method":"java.util.concurrent.ThreadPoolExecutor${"\$"}Worker.run","file":"ThreadPoolExecutor.java","lineNumber":641},{"method":"java.lang.Thread.run","file":"Thread.java","lineNumber":919}],"type":"ANDROID"}],"severity":"ERROR","breadcrumbs":[],"unhandled":true,"projectPackages":["com.example.testapp1mg"],"app":{"id":"com.example.testapp1mg","releaseStage":"development","version":"1.18.0","versionCode":"15"},"device":{"manufacturer":"Google","model":"Android SDK built for x86","osName":"android","osVersion":"10","cpuAbi":"x86","jailbroken":"true","locale":"en_US","totalMemory":"2089177088","runtimeVersions":{"androidApiLevel":"29","osBuild":"sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys"},"freeDisk":"483729408","freeMemory":"1168625664","orientation":"portrait","time":"2023-09-18T11:58:43.154Z"},"metadata":{"app":{"memoryUsage":5613392,"memoryTrimLevel":"None","totalMemory":5787501,"processName":"com.example.testapp1mg","name":"Sample Kotlin","memoryLimit":536870912,"lowMemory":false,"freeMemory":174109},"device":{"osBuild":"sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys","manufacturer":"Google","locationStatus":"allowed","networkAccess":"none","osVersion":"10","fingerprint":"google/sdk_gphone_x86/generic_x86:10/QSR1.210802.001/7603624:userdebug/dev-keys","model":"Android SDK built for x86","dpi":480,"screenResolution":"1776x1080","brand":"google","apiLevel":29,"batteryLevel":1.0,"cpuAbis":["x86"],"charging":false,"tags":"dev-keys","emulator":true,"screenDensity":3.0}}} - """.trimIndent() + """.trimIndent(), + ) + private val dummyLibraryMetadata = LibraryMetadata( + "test_lub", + "1.x.x", + "2", + "dummy_write_key", ) - private val dummyLibraryMetadata = LibraryMetadata("test_lub", "1.x.x", - "2", "dummy_write_key") private val jacksonAdapter = JacksonAdapter() private val gsonAdapter = GsonAdapter(GsonBuilder().disableHtmlEscaping().create()) + @Test - fun `error model serialization test`(){ + fun `error model serialization test`() { val errorModel = ErrorModel(dummyLibraryMetadata, errorEvents) val errorModelJackson = errorModel.serialize(jacksonAdapter) val errorModelGson = errorModel.serialize(gsonAdapter) - - assertThat(errorModelJackson, not(emptyString())) assertThat(errorModelGson, not(emptyString())) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoirTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoirTest.kt index 90cd5a337..a9dfe66a1 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoirTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultReservoirTest.kt @@ -57,43 +57,46 @@ class DefaultReservoirTest { const val MAX_COUNTERS = 20 const val MAX_LABEL_MAP_COUNT = 20 const val MAX_LABELS = 10 - } private lateinit var defaultStorage: DefaultReservoir - //create 200 metric + // create 200 metric private lateinit var testNameCounterMap: Map - //consider 1000 labels + // consider 1000 labels // private val testLabelMaps = (0 until MAX_LABEL_MAP_COUNT).map { // "testLabel_key_$it" to "testLabel_value_$it" // }.toMap() private lateinit var testLabels: List private lateinit var testCounterToLabelMap: Map - @Before fun initialize() { defaultStorage = DefaultReservoir( - ApplicationProvider.getApplicationContext(), false, TestExecutor() + ApplicationProvider.getApplicationContext(), + false,"test_db", + TestExecutor(), ) testNameCounterMap = (0 until MAX_COUNTERS).associate { val name = "testCounter_$it" name to LongCounter( - name, DefaultAggregatorHandler( - defaultStorage - ) + name, + DefaultAggregatorHandler( + defaultStorage, + ), ) } testLabels = (0..MAX_LABELS).map { val randomNumberOfPairs = Random.Default.nextInt( - 0, MAX_LABEL_MAP_COUNT + 0, + MAX_LABEL_MAP_COUNT, ) (0..randomNumberOfPairs).associate { val randomLabelIndex = Random.nextInt( - 0, MAX_LABEL_MAP_COUNT + 0, + MAX_LABEL_MAP_COUNT, ) "testLabel_key_$randomLabelIndex" to "testLabel_value_$randomLabelIndex" }.let { it } @@ -113,20 +116,26 @@ class DefaultReservoirTest { @Test fun insertOrIncrement() { testCounterToLabelMap.forEach { (counterName, labels) -> - //insert 1 as default value + // insert 1 as default value defaultStorage.insertOrIncrement( MetricModel( - counterName, MetricType.COUNTER, 1, labels - ) + counterName, + MetricType.COUNTER, + 1, + labels, + ), ) } var index = 0 - //increase counters by index + // increase counters by index testCounterToLabelMap.forEach { (counterName, labels) -> defaultStorage.insertOrIncrement( MetricModel( - counterName, MetricType.COUNTER, index.toLong(), labels - ) + counterName, + MetricType.COUNTER, + index.toLong(), + labels, + ), ) index++ @@ -139,46 +148,31 @@ class DefaultReservoirTest { assertThat(metric.name, Matchers.equalTo("testCounter_$index")) assertThat(metric.labels, Matchers.equalTo(testCounterToLabelMap[metric.name])) } - - } - - @Test - fun `test duplicate label insertion`() { - val labelEntities = (0..50).map { - LabelEntity("testLabel_key_$it", "testLabel_value_$it") - } - val labelDao = RudderDatabase.getDao(LabelEntity::class.java) - with(labelDao) { - val insertedIds = - labelEntities.insertSync(conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE) - assertThat(insertedIds?.size, Matchers.equalTo(51)) - assertThat(insertedIds, Matchers.not(Matchers.contains(-1))) - - val duplicateIds = - labelEntities.insertSync(conflictResolutionStrategy = Dao.ConflictResolutionStrategy.CONFLICT_IGNORE) - ?.toSet() - assertThat(duplicateIds?.size, Matchers.equalTo(1)) - assertThat(duplicateIds, Matchers.contains(-1)) - } } @Test fun `test insertion and reset for all`() { testCounterToLabelMap.forEach { (counterName, labels) -> - //insert 1 as default value + // insert 1 as default value defaultStorage.insertOrIncrement( MetricModel( - counterName, MetricType.COUNTER, 1, labels - ) + counterName, + MetricType.COUNTER, + 1, + labels, + ), ) } var index = 0 - //increase counters by index + // increase counters by index testCounterToLabelMap.forEach { (counterName, labels) -> defaultStorage.insertOrIncrement( MetricModel( - counterName, MetricType.COUNTER, index.toLong(), labels - ) + counterName, + MetricType.COUNTER, + index.toLong(), + labels, + ), ) index++ } @@ -195,7 +189,6 @@ class DefaultReservoirTest { assertThat(metric.name, Matchers.equalTo("testCounter_$index")) assertThat(metric.labels, Matchers.equalTo(testCounterToLabelMap[metric.name])) } - } @Test @@ -206,16 +199,17 @@ class DefaultReservoirTest { defaultStorage.getMetricsAndErrors(0, 0, 10) { metrics, errors -> assertThat(metrics, Matchers.empty()) assertThat( - errors, allOf( - hasSize(1), Matchers.contains( + errors, + allOf( + hasSize(1), + Matchers.contains( allOf( - hasProperty("errorEvent", Matchers.equalTo(TEST_ERROR_EVENTS_JSON)) - ) - ) - ) + hasProperty("errorEvent", Matchers.equalTo(TEST_ERROR_EVENTS_JSON)), + ), + ), + ), ) } - } @Test @@ -227,33 +221,44 @@ class DefaultReservoirTest { defaultStorage.getMetricsAndErrors(5, 5, 5) { metrics, errors -> assertThat(metrics, Matchers.empty()) assertThat( - errors, allOf( - hasSize(5), everyItem( + errors, + allOf( + hasSize(5), + everyItem( anyOf( hasProperty( - "errorEvent", Matchers.equalTo( - TestDataGenerator.getTestErrorEventJsonWithIdentity(6) - ) - ), hasProperty( - "errorEvent", Matchers.equalTo( - TestDataGenerator.getTestErrorEventJsonWithIdentity(7) - ) - ), hasProperty( - "errorEvent", Matchers.equalTo( - TestDataGenerator.getTestErrorEventJsonWithIdentity(8) - ) - ), hasProperty( - "errorEvent", Matchers.equalTo( - TestDataGenerator.getTestErrorEventJsonWithIdentity(9) - ) - ), hasProperty( - "errorEvent", Matchers.equalTo( - TestDataGenerator.getTestErrorEventJsonWithIdentity(10) - ) - ) - ) - ) - ) + "errorEvent", + Matchers.equalTo( + TestDataGenerator.getTestErrorEventJsonWithIdentity(6), + ), + ), + hasProperty( + "errorEvent", + Matchers.equalTo( + TestDataGenerator.getTestErrorEventJsonWithIdentity(7), + ), + ), + hasProperty( + "errorEvent", + Matchers.equalTo( + TestDataGenerator.getTestErrorEventJsonWithIdentity(8), + ), + ), + hasProperty( + "errorEvent", + Matchers.equalTo( + TestDataGenerator.getTestErrorEventJsonWithIdentity(9), + ), + ), + hasProperty( + "errorEvent", + Matchers.equalTo( + TestDataGenerator.getTestErrorEventJsonWithIdentity(10), + ), + ), + ), + ), + ), ) } } @@ -262,18 +267,23 @@ class DefaultReservoirTest { defaultStorage.clear() defaultStorage.insertOrIncrement( MetricModel( - "testCounter", MetricType.COUNTER, 1, mapOf("label" to "value") - ) + "testCounter", + MetricType.COUNTER, + 1, + mapOf("label" to "value"), + ), ) defaultStorage.getMetricsAndErrors(0, 0, 10) { metrics, errors -> assertThat( - metrics, allOf( - hasSize(1), Matchers.contains( + metrics, + allOf( + hasSize(1), + Matchers.contains( allOf( - hasProperty("name", Matchers.equalTo("testCounter")) - ) - ) - ) + hasProperty("name", Matchers.equalTo("testCounter")), + ), + ), + ), ) assertThat(errors, Matchers.empty()) } @@ -292,54 +302,69 @@ class DefaultReservoirTest { } defaultStorage.getMetricsAndErrors(0, 0, 30L) { metrics, errors -> assertThat( - metrics, allOf( - hasSize(20), everyItem( + metrics, + allOf( + hasSize(20), + everyItem( allOf( hasProperty( - "name", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray() - ) + "name", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray(), + ), ), hasProperty( - "type", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray() - ) + "type", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray(), + ), ), hasProperty( - "labels", hasEntry( + "labels", + hasEntry( + anyOf( + ( + insertMetricModels.map { + equalTo( + it.labels.keys.toList()[0], + ) + } + ), + + ), anyOf( - (insertMetricModels.map { - equalTo( - it.labels.keys.toList()[0] - ) - }) - - ), anyOf( - *(insertMetricModels.map { - equalTo( - it.labels.values.toList()[0] - ) - }).toTypedArray() - ) - ) + *( + insertMetricModels.map { + equalTo( + it.labels.values.toList()[0], + ) + } + ).toTypedArray(), + ), + ), ), - ) - ) - ) + ), + ), + ), ) assertThat( - errors, allOf( - hasSize(12), everyItem( + errors, + allOf( + hasSize(12), + everyItem( hasProperty( - "errorEvent", anyOf( - *(TestDataGenerator.generateTestErrorEventsJson(30).map { - Matchers.equalTo(it) - }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *( + TestDataGenerator.generateTestErrorEventsJson(30).map { + Matchers.equalTo(it) + } + ).toTypedArray(), + ), + ), + ), + ), ) } } @@ -357,52 +382,65 @@ class DefaultReservoirTest { } defaultStorage.getMetricsAndErrors(0, 0, 30L) { metrics, errors -> assertThat( - metrics, allOf( - hasSize(12), everyItem( + metrics, + allOf( + hasSize(12), + everyItem( allOf( hasProperty( - "name", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray() - ) + "name", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray(), + ), ), hasProperty( - "type", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray() - ) + "type", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray(), + ), ), hasProperty( - "labels", hasEntry( + "labels", + hasEntry( anyOf( - (insertMetricModels.map { - equalTo( - it.labels.keys.toList()[0] - ) - }) - - ), anyOf( - *(insertMetricModels.map { - equalTo( - it.labels.values.toList()[0] - ) - }).toTypedArray() - ) - ) + ( + insertMetricModels.map { + equalTo( + it.labels.keys.toList()[0], + ) + } + ), + + ), + anyOf( + *( + insertMetricModels.map { + equalTo( + it.labels.values.toList()[0], + ) + } + ).toTypedArray(), + ), + ), ), - ) - ) - ) + ), + ), + ), ) assertThat( - errors, allOf( - hasSize(20), everyItem( + errors, + allOf( + hasSize(20), + everyItem( hasProperty( - "errorEvent", anyOf( - *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray(), + ), + ), + ), + ), ) } } @@ -439,103 +477,129 @@ class DefaultReservoirTest { defaultStorage.resetMetricsFirst(10) defaultStorage.getMetricsAndErrors(0, 0, 10L) { metrics, errors -> assertThat( - metrics, allOf( - hasSize(10), everyItem( + metrics, + allOf( + hasSize(10), + everyItem( allOf( hasProperty( - "name", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray() - ) + "name", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray(), + ), ), hasProperty( - "value", equalTo(0L) + "value", + equalTo(0L), ), hasProperty( - "type", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray() - ) + "type", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray(), + ), ), hasProperty( - "labels", hasEntry( + "labels", + hasEntry( anyOf( - (insertMetricModels.map { - equalTo( - it.labels.keys.toList()[0] - ) - }) - - ), anyOf( - *(insertMetricModels.map { - equalTo( - it.labels.values.toList()[0] - ) - }).toTypedArray() - ) - ) + ( + insertMetricModels.map { + equalTo( + it.labels.keys.toList()[0], + ) + } + ), + + ), + anyOf( + *( + insertMetricModels.map { + equalTo( + it.labels.values.toList()[0], + ) + } + ).toTypedArray(), + ), + ), ), - ) - ) - ) + ), + ), + ), ) assertThat( - errors, allOf( - hasSize(8), everyItem( + errors, + allOf( + hasSize(8), + everyItem( hasProperty( - "errorEvent", anyOf( - *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray(), + ), + ), + ), + ), ) } val last2Metrics = listOf( - TestDataGenerator.getTestMetric(11), TestDataGenerator.getTestMetric(12) + TestDataGenerator.getTestMetric(11), + TestDataGenerator.getTestMetric(12), ) defaultStorage.getMetricsFirst(10, 2) { assertThat( - it, allOf( - hasSize(2), everyItem( + it, + allOf( + hasSize(2), + everyItem( allOf( hasProperty( - "name", anyOf( - *(last2Metrics.map { Matchers.equalTo(it.name) }).toTypedArray() - ) + "name", + anyOf( + *(last2Metrics.map { Matchers.equalTo(it.name) }).toTypedArray(), + ), ), hasProperty( - "value", anyOf( - *last2Metrics.map { Matchers.equalTo(it.value) }.toTypedArray() - ) + "value", + anyOf( + *last2Metrics.map { Matchers.equalTo(it.value) }.toTypedArray(), + ), ), hasProperty( - "type", anyOf( - *(last2Metrics.map { Matchers.equalTo(it.type) }).toTypedArray() - ) + "type", + anyOf( + *(last2Metrics.map { Matchers.equalTo(it.type) }).toTypedArray(), + ), ), hasProperty( - "labels", hasEntry( + "labels", + hasEntry( anyOf( - (last2Metrics.map { - equalTo( - it.labels.keys.toList()[0] - ) - }) - - ), anyOf( - *(last2Metrics.map { - equalTo( - it.labels.values.toList()[0] - ) - }).toTypedArray() - ) - ) + ( + last2Metrics.map { + equalTo( + it.labels.keys.toList()[0], + ) + } + ), + + ), + anyOf( + *( + last2Metrics.map { + equalTo( + it.labels.values.toList()[0], + ) + } + ).toTypedArray(), + ), + ), ), - ) - ) - ) + ), + ), + ), ) } } @@ -554,62 +618,75 @@ class DefaultReservoirTest { defaultStorage.resetMetricsFirst(12) defaultStorage.getMetricsAndErrors(0, 0, 12L) { metrics, errors -> assertThat( - metrics, allOf( - hasSize(10), everyItem( + metrics, + allOf( + hasSize(10), + everyItem( allOf( hasProperty( - "name", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray() - ) + "name", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.name) }).toTypedArray(), + ), ), hasProperty( - "value", equalTo(0L) + "value", + equalTo(0L), ), hasProperty( - "type", anyOf( - *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray() - ) + "type", + anyOf( + *(insertMetricModels.map { Matchers.equalTo(it.type) }).toTypedArray(), + ), ), hasProperty( - "labels", hasEntry( + "labels", + hasEntry( anyOf( - (insertMetricModels.map { - equalTo( - it.labels.keys.toList()[0] - ) - }) - - ), anyOf( - *(insertMetricModels.map { - equalTo( - it.labels.values.toList()[0] - ) - }).toTypedArray() - ) - ) + ( + insertMetricModels.map { + equalTo( + it.labels.keys.toList()[0], + ) + } + ), + + ), + anyOf( + *( + insertMetricModels.map { + equalTo( + it.labels.values.toList()[0], + ) + } + ).toTypedArray(), + ), + ), ), - ) - ) - ) + ), + ), + ), ) assertThat( - errors, allOf( - hasSize(8), everyItem( + errors, + allOf( + hasSize(8), + everyItem( hasProperty( - "errorEvent", anyOf( - *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray(), + ), + ), + ), + ), ) } } @Test - fun `test resetTillSync when metrics value more than dumped value`( - ) { + fun `test resetTillSync when metrics value more than dumped value`() { defaultStorage.clear() val insertMetricModels = TestDataGenerator.generateTestMetrics(10) val insertedErrors = TestDataGenerator.generateTestErrorEventsJson(8) @@ -619,11 +696,17 @@ class DefaultReservoirTest { insertedErrors.forEach { defaultStorage.saveError(ErrorEntity(it)) } - defaultStorage.resetTillSync(defaultStorage.getAllMetricsSync().map { - MetricModelWithId( - it.id, it.name, it.type, (it.value.toLong() - 2L), it.labels - ) - }) + defaultStorage.resetTillSync( + defaultStorage.getAllMetricsSync().map { + MetricModelWithId( + it.id, + it.name, + it.type, + (it.value.toLong() - 2L), + it.labels, + ) + }, + ) defaultStorage.getMetricsAndErrors(0, 0, 12L) { metrics, errors -> metrics.forEach { metricUnderTest -> val associatedMetric = insertMetricModels.firstOrNull { @@ -633,21 +716,23 @@ class DefaultReservoirTest { assertThat(metricUnderTest.value.toInt(), allOf(greaterThan(-1), lessThan(3))) } assertThat( - errors, allOf( - hasSize(8), everyItem( + errors, + allOf( + hasSize(8), + everyItem( hasProperty( - "errorEvent", anyOf( - *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray(), + ), + ), + ), + ), ) } } - fun `test resetTillSync when metrics value less than dumped value`( - ) { + fun `test resetTillSync when metrics value less than dumped value`() { defaultStorage.clear() val insertMetricModels = TestDataGenerator.generateTestMetrics(10) val insertedErrors = TestDataGenerator.generateTestErrorEventsJson(8) @@ -657,11 +742,17 @@ class DefaultReservoirTest { insertedErrors.forEach { defaultStorage.saveError(ErrorEntity(it)) } - defaultStorage.resetTillSync(defaultStorage.getAllMetricsSync().map { - MetricModelWithId( - it.id, it.name, it.type, (it.value.toLong() + 2L), it.labels - ) - }) + defaultStorage.resetTillSync( + defaultStorage.getAllMetricsSync().map { + MetricModelWithId( + it.id, + it.name, + it.type, + (it.value.toLong() + 2L), + it.labels, + ) + }, + ) defaultStorage.getMetricsAndErrors(0, 0, 12L) { metrics, errors -> metrics.forEach { metricUnderTest -> val associatedMetric = insertMetricModels.firstOrNull { @@ -671,15 +762,18 @@ class DefaultReservoirTest { assertThat(metricUnderTest.value, Matchers.equalTo(0L)) } assertThat( - errors, allOf( - hasSize(8), everyItem( + errors, + allOf( + hasSize(8), + everyItem( hasProperty( - "errorEvent", anyOf( - *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray() - ) - ) - ) - ) + "errorEvent", + anyOf( + *(insertedErrors.map { Matchers.equalTo(it) }).toTypedArray(), + ), + ), + ), + ), ) } } @@ -696,13 +790,15 @@ class DefaultReservoirTest { defaultStorage.getMetricsAndErrors(0, 0, 10) { metrics, errors -> assertThat(metrics, allOf(not(empty()), hasSize(10))) assertThat( - errors, allOf( - hasSize(1), Matchers.contains( + errors, + allOf( + hasSize(1), + Matchers.contains( allOf( - hasProperty("errorEvent", Matchers.equalTo(TEST_ERROR_EVENTS_JSON)) - ) - ) - ) + hasProperty("errorEvent", Matchers.equalTo(TEST_ERROR_EVENTS_JSON)), + ), + ), + ), ) } } @@ -772,9 +868,12 @@ class DefaultReservoirTest { defaultStorage.getMetricsAndErrors(0, 0, 10) { metrics, errors -> assertThat(metrics, allOf(not(empty()), hasSize(10))) assertThat( - errors, allOf( - not(empty()), hasSize(5), everyItem(hasProperty("id", not(`in`(ids)))) - ) + errors, + allOf( + not(empty()), + hasSize(5), + everyItem(hasProperty("id", not(`in`(ids)))), + ), ) } } @@ -798,9 +897,12 @@ class DefaultReservoirTest { defaultStorage.getMetricsAndErrors(0, 0, 10) { metrics, errors -> assertThat(metrics, allOf(not(empty()), hasSize(10))) assertThat( - errors, allOf( - not(empty()), hasSize(5), everyItem(hasProperty("id", not(`in`(ids)))) - ) + errors, + allOf( + not(empty()), + hasSize(5), + everyItem(hasProperty("id", not(`in`(ids)))), + ), ) } } @@ -836,4 +938,4 @@ class DefaultReservoirTest { } } -typealias Labels = Map \ No newline at end of file +typealias Labels = Map diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncerTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncerTest.kt index 75e1e0f42..a1fa98c1e 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncerTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncerTest.kt @@ -66,14 +66,14 @@ class DefaultSyncerTest { println("uploaded-m ${uploadedMetrics.size} cIndex-m $cumulativeIndexMetrics") println("uploaded-e ${uploadedErrorModel.eventsJson.size} cIndex-e $cumulativeIndexMErrors") if (cumulativeIndexMetrics > maxMetrics) { - assert(false) //should not reach here + assert(false) // should not reach here isMetricsDone.set(true) } else { val expected = getTestMetricList( cumulativeIndexMetrics, - (maxMetrics - cumulativeIndexMetrics).coerceAtMost(limit) + (maxMetrics - cumulativeIndexMetrics).coerceAtMost(limit), ) - if (expected.isNotEmpty()) + if (expected.isNotEmpty()) { assertThat( uploadedMetrics, allOf( @@ -81,30 +81,33 @@ class DefaultSyncerTest { not(empty()), hasSize((maxMetrics - cumulativeIndexMetrics).coerceAtMost(limit)), Matchers.contains>( - *(expected.toTypedArray()) - ) - ) + *(expected.toTypedArray()), + ), + ), ) - else + } else { assertThat(uploadedMetrics, empty()) + } if (cumulativeIndexMErrors < maxErrors) { val expectedSizeOfErrors = (maxErrors - cumulativeIndexMErrors).coerceAtMost(limit) assertThat( - uploadedErrorModel.eventsJson, allOf( + uploadedErrorModel.eventsJson, + allOf( notNullValue(), not(empty()), - hasSize(expectedSizeOfErrors) - ) + hasSize(expectedSizeOfErrors), + ), ) - }else + } else { assertThat(uploadedErrorModel.eventsJson, empty()) + } if (cumulativeIndexMetrics + uploadedMetrics.size == maxMetrics) { // Thread.sleep(1000) isMetricsDone.set(true) } if (cumulativeIndexMetrics + uploadedMetrics.size > maxMetrics) { - assert(false) //should not reach here + assert(false) // should not reach here isMetricsDone.set(true) } if (cumulativeIndexMErrors + uploadedErrorModel.eventsJson.size == maxErrors) { @@ -117,20 +120,16 @@ class DefaultSyncerTest { } cumulativeIndexMetrics += uploadedMetrics.size cumulativeIndexMErrors += uploadedErrorModel.eventsJson.size - - } syncer.startScheduledSyncs(interval, true, limit.toLong()) - Awaitility.await().atMost(4, TimeUnit.MINUTES).until{ + Awaitility.await().atMost(4, TimeUnit.SECONDS).until { isMetricsDone.get() && isErrorsDone.get() } syncer.stopScheduling() println("********checkSyncWithSuccess***********") - } - @Test fun `test sync with failure`() { println("********checkSyncWithFailure***********") @@ -146,35 +145,39 @@ class DefaultSyncerTest { val syncer = DefaultSyncer(mockReservoir, mockUploader, mockLibraryMetadata) val expectedMetrics = getTestMetricList( 0, - (maxMetrics).coerceAtMost(limit) + (maxMetrics).coerceAtMost(limit), ) val expectedSizeOfErrors = (maxErrors).coerceAtMost(limit) syncer.setCallback { uploadedMetrics, uploadedErrorModel, success -> - println("success: $success, uploaded metrics size: ${uploadedMetrics.size}, " + - "uploaded errors size: ${uploadedErrorModel.eventsJson.size}") + println( + "success: $success, uploaded metrics size: ${uploadedMetrics.size}, " + + "uploaded errors size: ${uploadedErrorModel.eventsJson.size}", + ) assertThat(success, `is`(false)) - assertThat(uploadedMetrics,Matchers.contains>( - *(expectedMetrics.toTypedArray()) - ) ) assertThat( - uploadedErrorModel.eventsJson, allOf( + uploadedMetrics, + Matchers.contains>( + *(expectedMetrics.toTypedArray()), + ), + ) + assertThat( + uploadedErrorModel.eventsJson, + allOf( notNullValue(), not(empty()), - hasSize(expectedSizeOfErrors) - ) + hasSize(expectedSizeOfErrors), + ), ) - //let's wait for 5 calls + // let's wait for 5 calls syncCounter.incrementAndGet() } syncer.startScheduledSyncs(interval, true, limit.toLong()) - Awaitility.await().atMost(2, TimeUnit.MINUTES).untilAtomic(syncCounter, equalTo(5)) + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAtomic(syncCounter, equalTo(5)) syncer.stopScheduling() println("********checkSyncWithFailure***********") - } - @Test fun stopScheduling() { val limit = 20 @@ -186,14 +189,14 @@ class DefaultSyncerTest { mockTheUploaderToSucceed() val syncer = DefaultSyncer(mockReservoir, mockUploader, mockLibraryMetadata) syncer.startScheduledSyncs(interval, true, limit.toLong()) - Thread.sleep(interval/2) //some time elapse before stopping + Thread.sleep(interval / 2) // some time elapse before stopping syncer.stopScheduling() - //waiting to stop + // waiting to stop Thread.sleep(interval + 10) - syncer.setCallback{ _, _, _ -> - assert(false) //call shouldn't reach here + syncer.setCallback { _, _, _ -> + assert(false) // call shouldn't reach here } - Thread.sleep(interval*5) + Thread.sleep(interval * 5) } @Test @@ -201,7 +204,6 @@ class DefaultSyncerTest { val scheduler = DefaultSyncer.Scheduler() var schedulerCalled = 0 scheduler.scheduleTimer(true, 500L) { - println("timer called") schedulerCalled++ } Thread.sleep(2100) @@ -215,7 +217,6 @@ class DefaultSyncerTest { val scheduler = DefaultSyncer.Scheduler() var schedulerCalled = 0 scheduler.scheduleTimer(false, 500L) { - println("timer called") schedulerCalled++ } Thread.sleep(2100) @@ -234,7 +235,7 @@ class DefaultSyncerTest { schedulerCalled++ } - //should run at 0. 500, 1000, 1500 + // should run at 0. 500, 1000, 1500 Thread.sleep(1100) // stopped during execution. scheduler.stop() Thread.sleep(1000) @@ -242,7 +243,6 @@ class DefaultSyncerTest { } private fun getTestMetricList(startPos: Int, limit: Int): List> { - return (startPos until startPos + limit).map { val index = it + 1 MetricModelWithId( @@ -250,44 +250,58 @@ class DefaultSyncerTest { "testMetric$it", MetricType.COUNTER, index.toLong(), - mapOf("testLabel_$it" to "testValue_$it") + mapOf("testLabel_$it" to "testValue_$it"), ) } } private fun mockTheUploaderToSucceed() { Mockito.`when`( mockUploader.upload( - org.mockito.kotlin.any(), org.mockito.kotlin.any(), org.mockito.kotlin.any() - ) + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + ), ).then { val callback = it.arguments[2] as ((Boolean) -> Unit) callback.invoke(true) } } - private var errorBeginIndex:Int = 0 + private var errorBeginIndex: Int = 0 private fun mockTheReservoir(maxMetrics: Int, maxErrors: Int) { Mockito.`when`( mockReservoir.getMetricsAndErrors( - Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), org.mockito.kotlin.any() - ) + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyLong(), + org.mockito.kotlin.any(), + ), ).then { - val callback = it.arguments[3] as (( - List>, List - ) -> Unit) + val callback = it.arguments[3] as ( + ( + List>, + List, + ) -> Unit + ) val skipMetrics = (it.arguments[0] as Long).toInt().coerceAtLeast(0) val skipError = (it.arguments[1] as Long).toInt().coerceAtLeast(0) + errorBeginIndex val limit = (it.arguments[2] as Long).toInt() - val metrics = if (skipMetrics < maxMetrics) getTestMetricList( - skipMetrics, - (maxMetrics - skipMetrics).coerceAtMost(limit), - ) else emptyList() + val metrics = if (skipMetrics < maxMetrics) { + getTestMetricList( + skipMetrics, + (maxMetrics - skipMetrics).coerceAtMost(limit), + ) + } else { + emptyList() + } // lastMetricsIndex += metrics.size - println("skipErrorsOffset(mock): $skipError, maxError: $maxErrors, limit: $limit") - callback.invoke(metrics, TestDataGenerator.generateTestErrorEventsJson( - skipError until (maxErrors).coerceAtMost(skipError + limit) - ).map { - ErrorEntity(it) - }) + callback.invoke( + metrics, + TestDataGenerator.generateTestErrorEventsJson( + skipError until (maxErrors).coerceAtMost(skipError + limit), + ).map { + ErrorEntity(it) + }, + ) } Mockito.`when`(mockReservoir.clearErrors(org.mockito.kotlin.any())).then { val idsToCLear = it.arguments[0] as Array @@ -298,16 +312,18 @@ class DefaultSyncerTest { private fun mockTheUploaderToFail() { Mockito.`when`( mockUploader.upload( - org.mockito.kotlin.any(), org.mockito.kotlin.any(), org.mockito.kotlin.any() - ) + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + ), ).then { val callback = it.arguments[2] as ((Boolean) -> Unit) callback.invoke(false) } } + @After fun tearDown() { errorBeginIndex = 0 } - -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploaderTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploaderTest.kt index 99cb2094a..5d4b7db02 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploaderTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultUploaderTest.kt @@ -14,7 +14,6 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal -import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.rudderstack.android.ruddermetricsreporterandroid.Configuration @@ -29,24 +28,27 @@ import com.rudderstack.rudderjsonadapter.JsonAdapter import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite -import org.mockito.Mockito import org.robolectric.annotation.Config -import java.util.Date @RunWith(AndroidJUnit4::class) @Config(sdk = [29]) open class DefaultUploaderTest { protected var jsonAdapter: JsonAdapter = MoshiAdapter() private val defaultUploader = DefaultUploadMediator( - ConfigModule(ContextModule(ApplicationProvider.getApplicationContext()), Configuration( - LibraryMetadata("test","1.0","4","abcde") - )),"https://some-api.com", - jsonAdapter, TestExecutor() + ConfigModule( + ContextModule(ApplicationProvider.getApplicationContext()), + Configuration( + LibraryMetadata("test", "1.0", "4", "abcde"), + ), + ), + "https://some-api.com", + jsonAdapter, + TestExecutor(), ) @Test fun upload() { - //TODO: add test for upload + // TODO: add test for upload } companion object { @@ -56,7 +58,6 @@ open class DefaultUploaderTest { private const val API_LEVEL = 28 private const val OS_BUILD = "Android" private const val ID = "id" - } } @@ -82,7 +83,6 @@ class DefaultUploaderTestMoshi : DefaultUploaderTest() { @Suite.SuiteClasses( DefaultUploaderTestGson::class, DefaultUploaderTestJackson::class, - DefaultUploaderTestMoshi::class + DefaultUploaderTestMoshi::class, ) -class DefaultUploaderTestSuite { -} \ No newline at end of file +class DefaultUploaderTestSuite diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfigTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfigTest.kt index f52f3a86a..9241b24ed 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfigTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/error/ImmutableConfigTest.kt @@ -19,7 +19,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.BreadcrumbType import com.rudderstack.android.ruddermetricsreporterandroid.error.CrashFilter import com.rudderstack.android.ruddermetricsreporterandroid.internal.NoopLogger import org.junit.Assert.* - import org.junit.Test class ImmutableConfigTest { @@ -31,7 +30,9 @@ class ImmutableConfigTest { val immutableConfig = ImmutableConfig( LibraryMetadata( "test_lib", - "1.3.0", "14", "my_write_key" + "1.3.0", + "14", + "my_write_key", ), listOf("com.rudderstack.android"), setOf(BreadcrumbType.ERROR), @@ -43,17 +44,20 @@ class ImmutableConfigTest { null, "test", null, - null + null, ) assertFalse(immutableConfig.shouldDiscardError(exception)) } + @Test fun `shouldDiscardError should return false for null crashFilter`() { val exception = Exception("test") val immutableConfig = ImmutableConfig( LibraryMetadata( "test_lib", - "1.3.0", "14", "my_write_key" + "1.3.0", + "14", + "my_write_key", ), listOf("com.rudderstack.android"), setOf(BreadcrumbType.ERROR), @@ -65,10 +69,11 @@ class ImmutableConfigTest { null, "test", null, - null + null, ) assertFalse(immutableConfig.shouldDiscardError(exception)) } + @Test fun `shouldDiscardError should return true for empty keywords crashFilter`() { val crashFilter = CrashFilter.generateWithKeyWords(emptyList()) @@ -76,7 +81,9 @@ class ImmutableConfigTest { val immutableConfig = ImmutableConfig( LibraryMetadata( "test_lib", - "1.3.0", "14", "my_write_key" + "1.3.0", + "14", + "my_write_key", ), listOf("com.rudderstack.android"), setOf(BreadcrumbType.ERROR), @@ -88,8 +95,8 @@ class ImmutableConfigTest { null, "test", null, - null + null, ) assertTrue(immutableConfig.shouldDiscardError(exception)) } -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestDataGenerator.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestDataGenerator.kt index 365ad69e0..d2bb22aaf 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestDataGenerator.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestDataGenerator.kt @@ -14,29 +14,31 @@ package com.rudderstack.android.ruddermetricsreporterandroid.utils -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.LongCounter import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType object TestDataGenerator { - fun generateTestMetrics(count : Int) = (1..count).map { + fun generateTestMetrics(count: Int) = (1..count).map { getTestMetric(it) } - fun getTestMetric(identity: Int) = MetricModel("test_metric_$identity", - MetricType.COUNTER, identity.toLong(), mapOf("type" to "type_$identity")) + fun getTestMetric(identity: Int) = MetricModel( + "test_metric_$identity", + MetricType.COUNTER, + identity.toLong(), + mapOf("type" to "type_$identity"), + ) - - fun generateTestErrorEventsJson(count : Int) = (1..count).map { + fun generateTestErrorEventsJson(count: Int) = (1..count).map { getTestErrorEventJsonWithIdentity(it) } fun generateTestErrorEventsJson(range: Iterable) = range.map { - getTestErrorEventJsonWithIdentity(it) - } - fun getTestErrorEventJsonWithIdentity(identity : Int) = """ + getTestErrorEventJsonWithIdentity(it) + } + fun getTestErrorEventJsonWithIdentity(identity: Int) = """ { "exceptions": [ { @@ -299,4 +301,4 @@ object TestDataGenerator { } """ -} \ No newline at end of file +} diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestExecutor.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestExecutor.kt index b1539fd8b..061111457 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestExecutor.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/TestExecutor.kt @@ -20,16 +20,16 @@ import java.util.concurrent.TimeUnit class TestExecutor : AbstractExecutorService() { private var _isShutdown = false override fun execute(command: Runnable?) { -command?.run() + command?.run() } override fun shutdown() { - //No op + // No op _isShutdown = true } override fun shutdownNow(): MutableList { - // No op + // No op shutdown() return mutableListOf() } @@ -45,4 +45,4 @@ command?.run() override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { return false } -} \ No newline at end of file +} diff --git a/samples/kotlin-jvm-app/.gitignore b/samples/kotlin-jvm-app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/samples/kotlin-jvm-app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/kotlin-jvm-app/build.gradle.kts b/samples/kotlin-jvm-app/build.gradle.kts new file mode 100644 index 000000000..6809b167b --- /dev/null +++ b/samples/kotlin-jvm-app/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("java-library") + alias(libs.plugins.kotlin.jvm) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + implementation(project(":core")) + implementation(project(":gsonrudderadapter")) +} diff --git a/samples/kotlin-jvm-app/src/main/java/com/rudderstack/android/kotlin_jvm_app/main.kt b/samples/kotlin-jvm-app/src/main/java/com/rudderstack/android/kotlin_jvm_app/main.kt new file mode 100644 index 000000000..741214aeb --- /dev/null +++ b/samples/kotlin-jvm-app/src/main/java/com/rudderstack/android/kotlin_jvm_app/main.kt @@ -0,0 +1,106 @@ +package com.rudderstack.android.kotlin_jvm_app + +import com.rudderstack.core.Analytics +import com.rudderstack.core.Configuration +import com.rudderstack.core.RudderUtils +import com.rudderstack.core.models.AliasMessage +import com.rudderstack.core.models.GroupMessage +import com.rudderstack.core.models.IdentifyMessage +import com.rudderstack.core.models.PageMessage +import com.rudderstack.core.models.ScreenMessage +import com.rudderstack.core.models.TrackMessage +import com.rudderstack.core.models.TrackProperties +import com.rudderstack.gsonrudderadapter.GsonAdapter + +private lateinit var analytics: Analytics +fun main(args: Array) { + analytics = Analytics( + "", + Configuration( + jsonAdapter = GsonAdapter(), + ) + ) + + makeAllEventsDirectlyUsingKotlinEventsAPI() +// makeAllEventsDirectlyUsingAndroidCompatibleEventsAPI() + + analytics.flush() +} + +fun makeAllEventsDirectlyUsingKotlinEventsAPI() { + val trackMessage = TrackMessage.create( + eventName = "Track Event 1", + properties = mapOf("key1" to "prop1", "key2" to "prop2"), + timestamp = RudderUtils.timeStamp, + ) + + val groupMessage = GroupMessage.create( + groupId = "testGroupId", + userId = "testUserId", + timestamp = RudderUtils.timeStamp, + groupTraits = mapOf("testKey" to "testValue"), + ) + + val screenMessage = ScreenMessage.create( + name = "testScreen", + category = "testCategory", + properties = mapOf("testKey" to "testValue"), + timestamp = RudderUtils.timeStamp, + ) + + val identify = IdentifyMessage.create( + userId = "testUserId", + traits = mapOf("testKey" to "testValue"), + timestamp = RudderUtils.timeStamp, + ) + + val alias = AliasMessage.create( + userId = "New Alias UserID", + previousId = "Old Alias UserID", + timestamp = RudderUtils.timeStamp, + ) + + val pageMessage = PageMessage.create( + anonymousId = "testAnonymousId", + userId = "testUserId", + timestamp = RudderUtils.timeStamp, + name = "testPage", + category = "testCategory", + properties = mapOf("testKey" to "testValue"), + ) + + analytics.processMessage(trackMessage, null) + analytics.processMessage(identify, null) + analytics.processMessage(screenMessage, null) + analytics.processMessage(groupMessage, null) + analytics.processMessage(alias, null) + analytics.processMessage(pageMessage, null) +} + +fun makeAllEventsDirectlyUsingAndroidCompatibleEventsAPI() { + analytics.track( + eventName = "Track Event 1", + trackProperties = TrackProperties("key1" to "prop1", "key2" to "prop2"), + ) + + analytics.identify( + userId = "testUserId", + traits = mapOf("testKey" to "testValue"), + ) + + analytics.screen( + screenName = "testScreen", + category = "testCategory", + screenProperties = TrackProperties("testKey" to "testValue"), + ) + + analytics.group( + groupId = "testGroupId", + groupTraits = TrackProperties("testKey" to "testValue"), + ) + + analytics.alias( + newId = "New Alias UserID", + previousId = "Old Alias UserID", + ) +} diff --git a/samples/sample-android-java/.gitignore b/samples/sample-android-java/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/samples/sample-android-java/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/sample-android-java/build.gradle b/samples/sample-android-java/build.gradle new file mode 100644 index 000000000..3d3787923 --- /dev/null +++ b/samples/sample-android-java/build.gradle @@ -0,0 +1,69 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:12 PM Last modified: 08/10/22, 4:12 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id 'com.android.application' +} + +android { + compileSdk 33 + + defaultConfig { + applicationId "com.rudderstack.android.sample_java" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + buildFeatures { + viewBinding true + } + namespace 'com.rudderstack.android.sample_java' +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment:2.6.0' + implementation 'androidx.navigation:navigation-ui:2.6.0' + + implementation project(path: ':android') + implementation project(path: ':moshirudderadapter') +// implementation project(path: ':gsonrudderadapter') +// implementation project(path: ':jacksonrudderadapter') + implementation project(path: ':repository') + implementation project(path: ':core') + implementation project(path: ':models') + implementation project(path: ':web') + implementation project(path: ':rudderjsonadapter') + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/samples/sample-android-java/proguard-rules.pro b/samples/sample-android-java/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/samples/sample-android-java/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/sample-android-java/src/androidTest/java/com/rudderstack/android/sample_java/ExampleInstrumentedTest.java b/samples/sample-android-java/src/androidTest/java/com/rudderstack/android/sample_java/ExampleInstrumentedTest.java new file mode 100644 index 000000000..3dcef9ea4 --- /dev/null +++ b/samples/sample-android-java/src/androidTest/java/com/rudderstack/android/sample_java/ExampleInstrumentedTest.java @@ -0,0 +1,40 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:12 PM Last modified: 08/10/22, 4:12 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sample_java; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.rudderstack.android.sample_java", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/samples/sample-android-java/src/main/AndroidManifest.xml b/samples/sample-android-java/src/main/AndroidManifest.xml new file mode 100644 index 000000000..54d40ef86 --- /dev/null +++ b/samples/sample-android-java/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/FirstFragment.java b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/FirstFragment.java new file mode 100644 index 000000000..2e8d7f895 --- /dev/null +++ b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/FirstFragment.java @@ -0,0 +1,61 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:12 PM Last modified: 08/10/22, 4:12 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sample_java; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + +import com.rudderstack.android.sample_java.databinding.FragmentFirstBinding; + +public class FirstFragment extends Fragment { + + private FragmentFirstBinding binding; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + + binding = FragmentFirstBinding.inflate(inflater, container, false); + return binding.getRoot(); + + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + binding.buttonFirst.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavHostFragment.findNavController(FirstFragment.this) + .navigate(R.id.action_FirstFragment_to_SecondFragment); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + +} \ No newline at end of file diff --git a/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/MainActivity.java b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/MainActivity.java new file mode 100644 index 000000000..d8062123a --- /dev/null +++ b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/MainActivity.java @@ -0,0 +1,95 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:12 PM Last modified: 08/10/22, 4:12 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sample_java; + +import android.os.Bundle; + +import com.google.android.material.snackbar.Snackbar; + +import androidx.appcompat.app.AppCompatActivity; + +import android.view.View; + +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; + +import com.rudderstack.android.sample_java.databinding.ActivityMainBinding; +import com.rudderstack.core.RudderOptions; + +import android.view.Menu; +import android.view.MenuItem; + +public class MainActivity extends AppCompatActivity { + + private AppBarConfiguration appBarConfiguration; + private ActivityMainBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setSupportActionBar(binding.toolbar); + + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); + appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); + + binding.fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //call track + SampleApplication.getAnalytics().track("fab pressed", new RudderOptions.Builder().build(), null, "user_id"); + Snackbar.make(view, "sent track message", Snackbar.LENGTH_LONG) + .setAction("Done", null).show(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + //call track + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onSupportNavigateUp() { + //track on back pressed + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); + return NavigationUI.navigateUp(navController, appBarConfiguration) + || super.onSupportNavigateUp(); + } +} \ No newline at end of file diff --git a/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SampleApplication.java b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SampleApplication.java new file mode 100644 index 000000000..141dbfce5 --- /dev/null +++ b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SampleApplication.java @@ -0,0 +1,92 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:54 PM Last modified: 08/10/22, 4:54 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sample_java; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.rudderstack.android.compat.RudderAnalyticsBuilderCompat; +import com.rudderstack.core.Analytics; +import com.rudderstack.core.BaseDestinationPlugin; +import com.rudderstack.core.Callback; +import com.rudderstack.core.Settings; +import com.rudderstack.core.compat.BaseDestinationPluginCompat; +import com.rudderstack.core.compat.PluginCompat; +import com.rudderstack.models.Message; +import com.rudderstack.moshirudderadapter.MoshiAdapter; + +import kotlin.Unit; + +public class SampleApplication extends Application { + private static Analytics analytics; + + public static Analytics getAnalytics() { + if(analytics == null){ + throw new NullPointerException("Application not initialized yet"); + } + return analytics; + } + + @Override + public void onCreate() { + super.onCreate(); + analytics = new RudderAnalyticsBuilderCompat(this, "", new Settings(), new MoshiAdapter()) + .withControlPlaneUrl("https:://www.cp.com") + .withDataPlaneUrl("https://hosted.rudderlaps.com") + .build(); + analytics.addPlugin(new PluginCompat() { + @NonNull + @Override + public Message intercept(@NonNull Chain chain) { + return chain.proceed(chain.message()); + } + }); + BaseDestinationPlugin bdp= new BaseDestinationPluginCompat("") { + @NonNull + @Override + public Message intercept(@NonNull Chain chain) { + return super.intercept(chain); + } + }; + bdp.addSubPlugin(new BaseDestinationPluginCompat.DestinationInterceptorCompat(){ + @NonNull + @Override + public Message intercept(@NonNull Chain chain) { + + return super.intercept(chain); + } + }); + analytics.addCallback(new Callback() { + @Override + public void success(@Nullable Message message) { + + } + + @Override + public void failure(@Nullable Message message, @Nullable Throwable throwable) { + + } + }); + analytics.applyClosure(plugin -> { + if(plugin instanceof BaseDestinationPlugin){ + ((BaseDestinationPlugin) plugin).setReady(true, null); + } + return Unit.INSTANCE; + }); + analytics.addPlugin(); + } +} diff --git a/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SecondFragment.java b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SecondFragment.java new file mode 100644 index 000000000..ca522c0e0 --- /dev/null +++ b/samples/sample-android-java/src/main/java/com/rudderstack/android/sample_java/SecondFragment.java @@ -0,0 +1,61 @@ +/* + * Creator: Debanjan Chatterjee on 08/10/22, 4:12 PM Last modified: 08/10/22, 4:12 PM + * Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.rudderstack.android.sample_java; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + +import com.rudderstack.android.sample_java.databinding.FragmentSecondBinding; + +public class SecondFragment extends Fragment { + + private FragmentSecondBinding binding; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + + binding = FragmentSecondBinding.inflate(inflater, container, false); + return binding.getRoot(); + + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + binding.buttonSecond.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavHostFragment.findNavController(SecondFragment.this) + .navigate(R.id.action_SecondFragment_to_FirstFragment); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + +} \ No newline at end of file diff --git a/samples/sample-android-java/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/sample-android-java/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..62aae673c --- /dev/null +++ b/samples/sample-android-java/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/sample-android-java/src/main/res/drawable/ic_launcher_background.xml b/samples/sample-android-java/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..7fda2acf6 --- /dev/null +++ b/samples/sample-android-java/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/sample-android-java/src/main/res/layout/activity_main.xml b/samples/sample-android-java/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..bd0824adb --- /dev/null +++ b/samples/sample-android-java/src/main/res/layout/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/sample-android-java/src/main/res/layout/content_main.xml b/samples/sample-android-java/src/main/res/layout/content_main.xml new file mode 100644 index 000000000..2086b8341 --- /dev/null +++ b/samples/sample-android-java/src/main/res/layout/content_main.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/samples/sample-android-java/src/main/res/layout/fragment_first.xml b/samples/sample-android-java/src/main/res/layout/fragment_first.xml new file mode 100644 index 000000000..8ca099de0 --- /dev/null +++ b/samples/sample-android-java/src/main/res/layout/fragment_first.xml @@ -0,0 +1,42 @@ + + + + + + + +