diff --git a/gsonrudderadapter/build.gradle b/gsonrudderadapter/build.gradle index 68791d9fe..46b6866da 100644 --- a/gsonrudderadapter/build.gradle +++ b/gsonrudderadapter/build.gradle @@ -41,6 +41,6 @@ dependencies { 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 +apply from: "$projectDir/../gradle/artifacts-jar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file diff --git a/jacksonrudderadapter/build.gradle b/jacksonrudderadapter/build.gradle index dfa280e9e..67e0286be 100644 --- a/jacksonrudderadapter/build.gradle +++ b/jacksonrudderadapter/build.gradle @@ -41,6 +41,6 @@ dependencies { 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 +apply from: "$projectDir/../gradle/artifacts-jar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file diff --git a/moshirudderadapter/build.gradle b/moshirudderadapter/build.gradle index 53d8893ee..fb745f175 100644 --- a/moshirudderadapter/build.gradle +++ b/moshirudderadapter/build.gradle @@ -42,6 +42,6 @@ dependencies { 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 +apply from: "$projectDir/../gradle/artifacts-jar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file diff --git a/repository/build.gradle b/repository/build.gradle index dd409dae2..baf6a8302 100644 --- a/repository/build.gradle +++ b/repository/build.gradle @@ -66,6 +66,6 @@ dependencies { 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 +apply from: "$projectDir/../gradle/artifacts-aar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file 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..6ce3cd184 100644 --- a/repository/src/main/java/com/rudderstack/android/repository/Dao.kt +++ b/repository/src/main/java/com/rudderstack/android/repository/Dao.kt @@ -55,13 +55,12 @@ class Dao( 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 @@ -151,10 +150,9 @@ class Dao( // receives the number of deleted rows and fires callback val extendedDeleteCb = { numberOfRows: Int -> deleteCallback?.invoke(numberOfRows) - if(_dataChangeListeners.isNotEmpty()) { - val allData = getAllSync() ?: listOf() + if (_dataChangeListeners.isNotEmpty()) { _dataChangeListeners.forEach { - it.onDataDeleted(this.subList(0, numberOfRows), allData) + it.onDataDeleted(this.subList(0, numberOfRows)) } } } @@ -199,14 +197,18 @@ class Dao( } } + fun deleteSync( + whereClause: String?, args: Array? + ): Int { + awaitDbInitialization() + return _db?.let { deleteFromDb(it, tableName, whereClause, args) } ?: -1 + } + internal fun deleteFromDb( - database: SQLiteDatabase, - tableName: String, whereClause: String?, args: Array? + database: SQLiteDatabase, tableName: String, whereClause: String?, args: Array? ): Int { return if (useContentProvider) context.contentResolver.delete( - entityContentProviderUri.build(), - whereClause, - args + entityContentProviderUri.build(), whereClause, args ) else synchronized(DB_LOCK) { database.openDatabase?.delete(tableName, whereClause, args) @@ -214,15 +216,14 @@ class Dao( } internal fun updateSync( - database: SQLiteDatabase, tableName: String, values: ContentValues?, + database: SQLiteDatabase, + tableName: String, + values: ContentValues?, selection: String?, selectionArgs: Array? ): Int { return if (useContentProvider) context.contentResolver.update( - entityContentProviderUri.build(), - values, - selection, - selectionArgs + entityContentProviderUri.build(), values, selection, selectionArgs ) else synchronized(DB_LOCK) { database.openDatabase?.update(tableName, values, selection, selectionArgs) @@ -292,7 +293,8 @@ class Dao( ) { runTransactionOrDeferToCreation { _: SQLiteDatabase -> callback.invoke( - runGetQuerySync(columns, selection, selectionArgs, orderBy, limit, offset) ?: listOf() + runGetQuerySync(columns, selection, selectionArgs, orderBy, limit, offset) + ?: listOf() ) } } @@ -312,20 +314,12 @@ class Dao( ): List? { awaitDbInitialization() return getItems( - _db ?: return null, - columns, - selection, - selectionArgs, - orderBy, - limit, - offset + _db ?: return null, columns, selection, selectionArgs, orderBy, limit, offset ) } fun getCount( - selection: String? = null, - selectionArgs: Array? = null, - callback: (Long) -> Unit + selection: String? = null, selectionArgs: Array? = null, callback: (Long) -> Unit ) { runTransactionOrDeferToCreation { db -> getCountSync(db, selection, selectionArgs).apply(callback) @@ -333,35 +327,26 @@ class Dao( } private fun getCountSync( - db: SQLiteDatabase, - selection: String? = null, - selectionArgs: Array? = null + db: SQLiteDatabase, selection: String? = null, selectionArgs: Array? = null ): Long { awaitDbInitialization() return if (useContentProvider) (context.contentResolver.query( - entityContentProviderUri.build(), - arrayOf("count(*)"), selection, selectionArgs, null - ) - ?.use { cursor -> + entityContentProviderUri.build(), arrayOf("count(*)"), selection, selectionArgs, null + )?.use { cursor -> cursor.moveToFirst() cursor.getLong(0) } ?: -1L) - else - synchronized(DB_LOCK) { - DatabaseUtils.queryNumEntries( - db, - tableName, - selection, - selectionArgs - ) + else synchronized(DB_LOCK) { + DatabaseUtils.queryNumEntries( + db, tableName, selection, selectionArgs + ) } } //create/update private fun insertData( - db: SQLiteDatabase, items: List, - conflictResolutionStrategy: ConflictResolutionStrategy + db: SQLiteDatabase, items: List, conflictResolutionStrategy: ConflictResolutionStrategy ): Pair, List> { synchronized(DB_LOCK) { if (!db.isOpen) return emptyList() to emptyList() @@ -373,9 +358,7 @@ class Dao( } private fun processEntityInsertion( - db: SQLiteDatabase, - conflictResolutionStrategy: ConflictResolutionStrategy, - items: List + db: SQLiteDatabase, conflictResolutionStrategy: ConflictResolutionStrategy, items: List ): Pair, List> { var (autoIncrementFieldName: String?, nextValue: Long) = getAutoIncrementFieldToNextValue(db) var dbCount = @@ -384,42 +367,36 @@ class Dao( ) 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) - } + if (!useContentProvider) { + 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 = synchronized(DB_LOCK){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 @@ -428,13 +405,10 @@ class Dao( //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 + db, tableName, autoIncField.fieldName ) + 1L } ?: (null to 0L) @@ -449,19 +423,11 @@ class Dao( //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 + db: SQLiteDatabase, tableName: String, column: String ): Long { return synchronized(DB_LOCK) { db.query( - tableName, - arrayOf("IFNULL(MAX($column), 0)"), - null, - null, - null, - null, - null + tableName, arrayOf("IFNULL(MAX($column), 0)"), null, null, null, null, null ) }.let { cursor -> (if (cursor.moveToFirst()) { @@ -474,26 +440,22 @@ class Dao( internal fun insertContentValues( database: SQLiteDatabase, - tableName: String, contentValues: ContentValues, nullHackColumn: String?, + tableName: String, + contentValues: ContentValues, + nullHackColumn: String?, conflictAlgorithm: Int ): Long { return if (useContentProvider) (context.contentResolver.insert( - entityContentProviderUri - .appendQueryParameter( - EntityContentProvider.ECP_CONFLICT_RESOLUTION_CODE, - conflictAlgorithm.toString() - ) - .build(), contentValues + entityContentProviderUri.appendQueryParameter( + EntityContentProvider.ECP_CONFLICT_RESOLUTION_CODE, conflictAlgorithm.toString() + ).build(), contentValues )?.let { it.lastPathSegment?.toLongOrNull() } ?: -1) else { (database.openDatabase?.insertWithOnConflict( - tableName, - nullHackColumn, - contentValues, - conflictAlgorithm + tableName, nullHackColumn, contentValues, conflictAlgorithm ) ?: -1) } @@ -511,25 +473,25 @@ class Dao( ): List { //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 - ) - else synchronized(DB_LOCK) { - db.openDatabase?.query( - tableName, - columns, - selection, - selectionArgs, - null, - null, - orderBy, - if (offset != null) "$offset,$limit" else limit - ) - }) ?: return listOf() + ?: 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 + ) + }) ?: return listOf() val items = ArrayList(cursor.count) @@ -583,14 +545,14 @@ class Dao( _db = sqLiteDatabase todoLock.lock() } - while (todoTransactions.isNotEmpty()){ + while (todoTransactions.isNotEmpty()) { try { executorService.takeUnless { it.isShutdown }?.submit( todoTransactions.poll( 50, TimeUnit.MILLISECONDS ) ) - }catch (ex: InterruptedException){ + } catch (ex: InterruptedException) { ex.printStackTrace() } } @@ -618,14 +580,13 @@ class Dao( _db?.openDatabase?.endTransaction() } - fun execTransaction(transaction : () -> Unit){ - synchronized(DB_LOCK){ - beginTransaction() - transaction.invoke() - setTransactionSuccessful() - endTransaction() - } + fun execTransaction(transaction: () -> Unit) { + beginTransaction() + transaction.invoke() + setTransactionSuccessful() + endTransaction() } + fun execSql(command: String, callback: (() -> Unit)? = null) { runTransactionOrDeferToCreation { db: SQLiteDatabase -> @@ -634,18 +595,16 @@ class Dao( 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 + // 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 @@ -661,8 +620,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 +645,18 @@ 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( - cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: throw IllegalArgumentException("No such column $fieldName") - ) + RudderField.Type.INTEGER -> if (isNullable) cursor.getLongOrNull(cursor.getColumnIndex( + fieldName + ).takeIf { it >= 0 } ?: throw IllegalArgumentException( + "No such column $fieldName" + )) else cursor.getLong(cursor.getColumnIndex(fieldName).takeIf { it >= 0 } + ?: 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( - cursor.getColumnIndex(fieldName).takeIf { it >= 0 } - ?: 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(cursor.getColumnIndex(fieldName).takeIf { it >= 0 } + ?: throw IllegalArgumentException("No such column $fieldName")) } private val SQLiteDatabase.openDatabase @@ -765,13 +721,13 @@ 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 */ diff --git a/rudderjsonadapter/build.gradle b/rudderjsonadapter/build.gradle index 884951230..a8a35c49a 100644 --- a/rudderjsonadapter/build.gradle +++ b/rudderjsonadapter/build.gradle @@ -38,6 +38,6 @@ 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 +apply from: "$projectDir/../gradle/artifacts-jar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file diff --git a/rudderreporter/build.gradle b/rudderreporter/build.gradle index 5f6a58675..dabf55f9a 100644 --- a/rudderreporter/build.gradle +++ b/rudderreporter/build.gradle @@ -92,6 +92,6 @@ dependencies { // 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 +apply from: "$projectDir/../gradle/artifacts-aar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file diff --git a/rudderreporter/proguard-rules.pro b/rudderreporter/proguard-rules.pro index e00012764..7a467838d 100644 --- a/rudderreporter/proguard-rules.pro +++ b/rudderreporter/proguard-rules.pro @@ -34,4 +34,5 @@ -dontwarn com.fasterxml.jackson.annotation.JsonIgnore -keep class com.rudderstack.android.ruddermetricsreporterandroid.models.LabelEntity { *; } -keep class com.rudderstack.android.ruddermetricsreporterandroid.models.MetricEntity { *; } +-keep class com.rudderstack.android.ruddermetricsreporterandroid.models.SnapshotEntity { *; } -keep class com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity { *; } \ 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..3fa80146f 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/DefaultRudderReporter.kt @@ -19,13 +19,14 @@ 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.DefaultPeriodicSyncer import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultReservoir -import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultSyncer +import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultSnapshotCapturer import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultUploadMediator import com.rudderstack.android.ruddermetricsreporterandroid.internal.NetworkChangeCallback +import com.rudderstack.android.ruddermetricsreporterandroid.internal.DefaultSnapshotCreator import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ConfigModule import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ContextModule import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.SystemServiceModule @@ -38,12 +39,13 @@ import java.util.concurrent.Executors class DefaultRudderReporter( private val _metrics: Metrics, private val _errorClient: ErrorClient?, - override val syncer: Syncer, + override val syncer: PeriodicSyncer ) : RudderReporter { private var connectivity: Connectivity? = null private var backgroundTaskService: BackgroundTaskService? = null + @JvmOverloads constructor( context: Context, @@ -61,8 +63,9 @@ class DefaultRudderReporter( baseUrl, configuration, jsonAdapter, - isMetricsEnabled, isErrorEnabled, - networkExecutor?:Executors.newCachedThreadPool(), + isMetricsEnabled, + isErrorEnabled, + networkExecutor ?: Executors.newCachedThreadPool(), backgroundTaskService ?: BackgroundTaskService(), useContentProvider, isGzipEnabled @@ -133,8 +136,9 @@ class DefaultRudderReporter( contextModule, DefaultReservoir(contextModule.ctx, useContentProvider), configuration, - DefaultUploadMediator(configModule, baseUrl, jsonAdapter, networkExecutor, - isGzipEnabled = isGzipEnabled), + DefaultUploadMediator( + baseUrl, jsonAdapter, networkExecutor, isGzipEnabled = isGzipEnabled + ), jsonAdapter, memoryTrimState, isMetricsAggregatorEnabled, @@ -157,7 +161,13 @@ class DefaultRudderReporter( reservoir, configuration, ConfigModule(contextModule, configuration), - DefaultSyncer(reservoir, uploadMediator, configuration.libraryMetadata), + DefaultPeriodicSyncer( + reservoir, uploadMediator, DefaultSnapshotCapturer( + DefaultSnapshotCreator( + configuration.libraryMetadata, apiVersion, jsonAdapter + ) + ) + ), jsonAdapter, memoryTrimState, isMetricsEnabled, @@ -170,31 +180,40 @@ class DefaultRudderReporter( reservoir: Reservoir, configuration: Configuration, configModule: ConfigModule, - syncer: Syncer, + periodicSyncer: PeriodicSyncer, jsonAdapter: JsonAdapter, memoryTrimState: MemoryTrimState, isMetricsEnabled: Boolean = true, isErrorEnabled: Boolean = true, backgroundTaskService: BackgroundTaskService? = null - ):this(contextModule, reservoir, configuration, configModule, syncer, jsonAdapter, + ) : this( + contextModule, + reservoir, + configuration, + configModule, + periodicSyncer, + jsonAdapter, memoryTrimState, - ConnectivityCompat(contextModule.ctx, RudderReporterNetworkChangeCallback(syncer)), - isMetricsEnabled, isErrorEnabled, backgroundTaskService) + ConnectivityCompat(contextModule.ctx, RudderReporterNetworkChangeCallback(periodicSyncer)), + isMetricsEnabled, + isErrorEnabled, + backgroundTaskService + ) private constructor( contextModule: ContextModule, reservoir: Reservoir, configuration: Configuration, configModule: ConfigModule, - syncer: Syncer, + periodicSyncer: PeriodicSyncer, jsonAdapter: JsonAdapter, memoryTrimState: MemoryTrimState, connectivity: Connectivity, isMetricsEnabled: Boolean = true, isErrorEnabled: Boolean = true, backgroundTaskService: BackgroundTaskService? = null - ): this( - DefaultMetrics(DefaultAggregatorHandler(reservoir, isMetricsEnabled), syncer), + ) : this( + DefaultMetrics(DefaultAggregatorHandler(reservoir, isMetricsEnabled), periodicSyncer), DefaultErrorClient( contextModule, configuration, configModule, DataCollectionModule( contextModule, @@ -205,7 +224,7 @@ class DefaultRudderReporter( memoryTrimState ), reservoir, jsonAdapter, memoryTrimState, isErrorEnabled ), - syncer + periodicSyncer ) { this.connectivity = connectivity this.backgroundTaskService = backgroundTaskService @@ -227,13 +246,13 @@ class DefaultRudderReporter( //call unregister on shutdown - internal class RudderReporterNetworkChangeCallback(private val syncer: Syncer) : + internal class RudderReporterNetworkChangeCallback(private val periodicSyncer: PeriodicSyncer) : NetworkChangeCallback { override fun invoke(hasConnection: Boolean, networkState: String) { if (hasConnection) { try { - syncer.flushAllMetrics() + periodicSyncer.flushAllMetrics() } catch (ex: Exception) { ex.printStackTrace() } @@ -241,14 +260,16 @@ class DefaultRudderReporter( } } -// companion object{ -// internal fun JsonAdapter.manipulate(): JsonAdapter { + companion object { + // internal fun JsonAdapter.manipulate(): JsonAdapter { // if(this is MoshiAdapter){ // add(CustomDateAdapterMoshi()) // } // return this // } -// } + private const val apiVersion: Int = 1; + + } } 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..825a432e7 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,13 @@ 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():PeriodicSyncer /** * Enables or disables recording of metrics. However unless shut down, already recorded @@ -39,4 +38,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/Syncer.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/PeriodicSyncer.kt similarity index 76% rename from rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt rename to rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/PeriodicSyncer.kt index 903a98f95..5e6eb92f1 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Syncer.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/PeriodicSyncer.kt @@ -14,16 +14,18 @@ package com.rudderstack.android.ruddermetricsreporterandroid -import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot -interface Syncer { +interface PeriodicSyncer { + @Deprecated("Use startPeriodicSyncs instead") fun startScheduledSyncs( interval: Long, flushOnStart: Boolean, flushCount: Long ) + fun startPeriodicSyncs( + interval: Long, flushOnStart: Boolean, flushCount: Long + ) //setting null will nullify the callback - fun setCallback(callback: ((uploaded: List>, - uploadedErrorModel: ErrorModel, + fun setCallback(callback: ((uploadedSnapshot: Snapshot, success: Boolean) -> Unit)?) fun stopScheduling() 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..5dd6527e0 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/Reservoir.kt @@ -17,20 +17,31 @@ package com.rudderstack.android.ruddermetricsreporterandroid import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModelWithId import com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot 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 getMetricsAndErrorFirst(limit : Long, callback : (List>, List) -> Unit) - fun getMetricsCount(callback : (Long) -> Unit) + fun getMetricsFirstSync(limit: Long): List> + fun getMetricsFirstSync(skip: Long, 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 clear() fun clearMetrics() fun resetMetricsFirst(limit: Long) @@ -38,15 +49,35 @@ 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) + fun clearErrorsSync(ids: Array) + + fun saveSnapshot(snapshot: Snapshot, callback: ((Long) -> Unit)?= null) + fun saveSnapshotSync(snapshot: Snapshot) : Long + fun getAllSnapshotsSync(): List + fun getAllSnapshots(callback: (List) -> Unit) + + fun getSnapshots(limit: Long, offset: Int = 0): List + fun deleteSnapshots(snapshotIds: List, callback: ((numberOfRows: Int) -> Unit)?= null) + + /** + * Deletes the snapshots with the given ids + * + * @param snapshotIds : List of snapshot ids + * @return number of rows deleted + */ + fun deleteSnapshotsSync(snapshotIds: List) : Int + fun clearSnapshots() /** * Will reset each element up to the value 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..83c638189 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/RudderReporter.kt @@ -19,6 +19,6 @@ import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorClient interface RudderReporter { val metrics: Metrics val errorClient : ErrorClient - val syncer: Syncer + val syncer: PeriodicSyncer fun shutdown() } \ No newline at end of file diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCapturer.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCapturer.kt new file mode 100644 index 000000000..16541d3c3 --- /dev/null +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCapturer.kt @@ -0,0 +1,25 @@ +/* + * Creator: Debanjan Chatterjee on 02/11/23, 5:08 pm Last modified: 02/11/23, 5:08 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.ruddermetricsreporterandroid + +interface SnapshotCapturer { + + fun captureSnapshotsAndResetReservoir(batchItemCount: Long, reservoir: Reservoir) : Int + fun captureSnapshotsAndResetReservoir(batchItemCount: Long, reservoir: Reservoir, callback : + (totalBatches: Int) -> Unit) + + fun shutdown() +} + diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCreator.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCreator.kt new file mode 100644 index 000000000..188ec9b27 --- /dev/null +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/SnapshotCreator.kt @@ -0,0 +1,23 @@ +/* + * Creator: Debanjan Chatterjee on 10/11/23, 12:53 pm Last modified: 10/11/23, 12:53 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.ruddermetricsreporterandroid + +import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot + +fun interface SnapshotCreator { + fun createSnapshot(metrics: List>, errorEvents: List): + Snapshot? +} \ 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..b5c84489b 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/UploadMediator.kt @@ -14,13 +14,11 @@ package com.rudderstack.android.ruddermetricsreporterandroid -import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot fun interface UploadMediator { fun upload( - metrics: List>, - error: ErrorModel, + snapshot: Snapshot, callback: (success: Boolean) -> Unit ) } \ No newline at end of file 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..d7e56eba4 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 @@ -19,6 +19,7 @@ import com.rudderstack.android.repository.EntityFactory import com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity import com.rudderstack.android.ruddermetricsreporterandroid.models.LabelEntity import com.rudderstack.android.ruddermetricsreporterandroid.models.MetricEntity +import com.rudderstack.android.ruddermetricsreporterandroid.models.SnapshotEntity class DefaultEntityFactory : EntityFactory { @@ -27,6 +28,7 @@ class DefaultEntityFactory : EntityFactory { MetricEntity::class.java -> MetricEntity.create(values) as? T? LabelEntity::class.java -> LabelEntity.create(values) as? T? ErrorEntity::class.java -> ErrorEntity.create(values) as? T? + SnapshotEntity::class.java -> SnapshotEntity.create(values) as? T? else -> null } } 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..17959eb38 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 @@ -15,19 +15,19 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal import com.rudderstack.android.ruddermetricsreporterandroid.Metrics -import com.rudderstack.android.ruddermetricsreporterandroid.Syncer +import com.rudderstack.android.ruddermetricsreporterandroid.PeriodicSyncer import com.rudderstack.android.ruddermetricsreporterandroid.internal.metrics.DefaultMeter 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 { +private val periodicSyncer: PeriodicSyncer) : Metrics { override fun getMeter(): Meter { return DefaultMeter(aggregatorHandler) } @Deprecated("Use [RudderReporter.syncer] instead") - override fun getSyncer(): Syncer { - return syncer + override fun getSyncer(): PeriodicSyncer { + return periodicSyncer } override fun enable(enable: Boolean) { @@ -36,6 +36,6 @@ private val syncer: Syncer) : Metrics { override fun shutdown() { // aggregatorHandler.shutdown() - syncer.stopScheduling() + periodicSyncer.stopScheduling() } } \ 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/DefaultPeriodicSyncer.kt similarity index 52% rename from rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt rename to rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultPeriodicSyncer.kt index c542254ad..432f13017 100644 --- a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSyncer.kt +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultPeriodicSyncer.kt @@ -14,25 +14,24 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal -import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir -import com.rudderstack.android.ruddermetricsreporterandroid.Syncer +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCapturer +import com.rudderstack.android.ruddermetricsreporterandroid.PeriodicSyncer import com.rudderstack.android.ruddermetricsreporterandroid.UploadMediator -import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModelWithId import java.util.Timer import java.util.TimerTask import java.util.concurrent.atomic.AtomicBoolean -class DefaultSyncer internal constructor( +class DefaultPeriodicSyncer internal constructor( private val reservoir: Reservoir, private val uploader: UploadMediator, - private val libraryMetadata: LibraryMetadata -) : Syncer { - private var _callback: ((uploadedMetrics: List>, - uploadedErrorModel: ErrorModel, - success: Boolean) -> Unit)? = null + private val snapshotCapturer: SnapshotCapturer, +// private val libraryMetadata: LibraryMetadata +) : PeriodicSyncer { + private var _callback: (( + snapshot: Snapshot, success: Boolean + ) -> Unit)? = null set(value) { synchronized(this) { field = value @@ -44,14 +43,32 @@ class DefaultSyncer internal constructor( private var flushCount = DEFAULT_FLUSH_SIZE private val scheduler = Scheduler() - override fun startScheduledSyncs( - interval: Long, flushOnStart: Boolean, - flushCount: Long + @Deprecated("Use startPeriodicSyncs instead", + ReplaceWith("startPeriodicSyncs(interval, flushOnStart, flushCount)") + ) + override fun startScheduledSyncs(interval: Long, flushOnStart: Boolean, flushCount: Long) { + startPeriodicSyncs(interval, flushOnStart, flushCount) + } + + override fun startPeriodicSyncs( + interval: Long, flushOnStart: Boolean, flushCount: Long ) { this.flushCount = flushCount _isShutDown.set(false) scheduler.scheduleTimer(flushOnStart, interval) { - flushAllMetrics() + captureSnapshotAndFlush(flushCount) + } + } + + private fun captureSnapshotAndFlush(flushCount: Long) { + snapshotCapturer.captureSnapshotsAndResetReservoir(flushCount, reservoir) { + flushAllSnapshots() + } + } + private fun flushAllSnapshots(){ + if (_isShutDown.get()) return + if (_atomicRunning.compareAndSet(false, true)) { + uploadSnapshots() } } @@ -64,66 +81,53 @@ 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: (( + snapshot: Snapshot, success: Boolean + ) -> Unit)? + ) { this._callback = callback } - private fun flush(flushCount: Long) { - flush(0L, flushCount) - } - private fun flush(startIndex: Long, flushCount: Long) { - reservoir.getMetricsAndErrors(startIndex,0, flushCount) { metrics, errors -> - val validMetrics = metrics.filterWithValidValues() - if (validMetrics.isEmpty() && errors.isEmpty()) { - _atomicRunning.set(false) - if (_isShutDown.get()) - stopScheduling() - return@getMetricsAndErrors - } - val errorModel = ErrorModel(libraryMetadata, errors.map { it.errorEvent }) - uploader.upload(validMetrics, errorModel) { success -> - if (success) { - reservoir.resetTillSync(validMetrics) - reservoir.clearErrors(errors.map { it.id }.toTypedArray()) - } - synchronized(this) { - _callback?.invoke(validMetrics, errorModel, success) - } - if (_isShutDown.get()) { + private fun uploadSnapshots() { + val snapshotToUpload = reservoir.getSnapshots(1) + if (snapshotToUpload.isNotEmpty()) { + uploader.upload(snapshotToUpload.first()) { + if (it) { + reservoir.deleteSnapshots(snapshotToUpload.map { it.id }) + _callback?.invoke(snapshotToUpload.first(), true) + if (_isShutDown.get()) { + _atomicRunning.set(false) + stopScheduling() + return@upload + } + uploadSnapshots() + } else { + _callback?.invoke(snapshotToUpload.first(), false) _atomicRunning.set(false) - stopScheduling() - return@upload + if (_isShutDown.get()) stopScheduling() } - if(success) - flush(startIndex + flushCount, flushCount) - else - _atomicRunning.set(false) } } } override fun stopScheduling() { _isShutDown.set(true) - if (_atomicRunning.get()) - return + if (_atomicRunning.get()) return scheduler.stop() + snapshotCapturer.shutdown() } override fun flushAllMetrics() { - if (_isShutDown.get()) - return - if (_atomicRunning.compareAndSet(false, true)) { - flush(flushCount) - } + if (_isShutDown.get()) return + captureSnapshotAndFlush(flushCount) } companion object { 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 @@ -136,20 +140,15 @@ class DefaultSyncer internal constructor( } } thresholdCountDownTimer.scheduleAtFixedRate( - periodicTaskScheduler, - if (callbackOnStart) 0 else flushInterval, - flushInterval + periodicTaskScheduler, if (callbackOnStart) 0 else flushInterval, flushInterval ) } - fun stop(){ + + fun stop() { periodicTaskScheduler?.cancel() thresholdCountDownTimer.cancel() } } - private fun List>.filterWithValidValues(): List> { - return this.filter { - it.value.toLong() > 0 - } - } + } \ 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..54b8ad77d 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 @@ -19,12 +19,14 @@ import androidx.annotation.VisibleForTesting import com.rudderstack.android.repository.Dao import com.rudderstack.android.repository.RudderDatabase import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModelWithId import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType import com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity import com.rudderstack.android.ruddermetricsreporterandroid.models.LabelEntity import com.rudderstack.android.ruddermetricsreporterandroid.models.MetricEntity +import com.rudderstack.android.ruddermetricsreporterandroid.models.SnapshotEntity import java.math.BigDecimal import java.math.BigInteger import java.util.concurrent.ExecutorService @@ -34,12 +36,13 @@ import kotlin.math.pow class DefaultReservoir @JvmOverloads constructor( androidContext: Context, useContentProvider: Boolean, - private val dbExecutor: ExecutorService? = null + dbExecutor: ExecutorService? = null ) : Reservoir { private val dbName = "metrics_db_${androidContext.packageName}.db" private val metricDao: Dao private val labelDao: Dao private val errorDao: Dao + private val snapshotDao: Dao private var _storageListeners = listOf() private val maxErrorCount = AtomicLong(MAX_ERROR_COUNT) @@ -55,6 +58,7 @@ class DefaultReservoir @JvmOverloads constructor( metricDao = RudderDatabase.getDao(MetricEntity::class.java) labelDao = RudderDatabase.getDao(LabelEntity::class.java) errorDao = RudderDatabase.getDao(ErrorEntity::class.java) + snapshotDao = RudderDatabase.getDao(SnapshotEntity::class.java) } override fun insertOrIncrement( @@ -144,7 +148,6 @@ class DefaultReservoir @JvmOverloads constructor( 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 + "'" + ";" ) @@ -166,6 +169,19 @@ class DefaultReservoir @JvmOverloads constructor( } } + override fun getMetricsFirstSync(skip: Long, limit: Long): List> { + with(metricDao) { + val metricEntities = runGetQuerySync(limit = limit.toString(), + offset = if (skip > 0) skip.toString() else null) + return metricEntities?.map { + val labels = getLabelsForMetric(it) + MetricModelWithId( + it.id.toString(), it.name, MetricType.getType(it.type), it.value, labels + ) + } ?: listOf() + } + } + override fun getMetricsFirst( skip: Long, limit: Long, callback: (List>) -> Unit ) { @@ -217,6 +233,7 @@ class DefaultReservoir @JvmOverloads constructor( override fun clear() { clearErrors() clearMetrics() + clearSnapshots() } override fun clearMetrics() { @@ -308,6 +325,77 @@ class DefaultReservoir @JvmOverloads constructor( } + override fun clearErrorsSync(ids: Array) { + errorDao.deleteSync( + whereClause = "${ + ErrorEntity.ColumnNames.ID + } IN (${ids.joinToString(",") { it.toString() }})", null + ) + } + + override fun saveSnapshot(snapshot: Snapshot, callback: ((Long) -> Unit)?) { + with(snapshotDao) { + val snapshotEntity = SnapshotEntity( + snapshot + ) + listOf(snapshotEntity).insert { + callback?.invoke(it.firstOrNull()?:0L) + } + } + } + + override fun saveSnapshotSync(snapshot: Snapshot) : Long { + with(snapshotDao){ + val snapshotEntity = SnapshotEntity( + snapshot + ) + return listOf(snapshotEntity).insertSync()?.firstOrNull() ?: -1L + } + } + + override fun getAllSnapshotsSync(): List { + return snapshotDao.getAllSync()?.map { + it.toSnapshot() + } ?: listOf() + } + + override fun getAllSnapshots(callback: (List) -> Unit) { + with(snapshotDao) { + runGetQuery { snapshotEntities -> + callback(snapshotEntities.map { + it.toSnapshot() + }) + } + } + } + + override fun getSnapshots(limit: Long, offset: Int): List { + return snapshotDao.runGetQuerySync(limit = limit.toString(), offset = offset.toString()) + ?.map { + it.toSnapshot() + } ?: listOf() + } + + override fun deleteSnapshots(snapshotIds: List, callback: ((Int) -> Unit)?) { + snapshotDao.delete( + whereClause = "${ + SnapshotEntity.ColumnNames.ID + } IN (${snapshotIds.joinToString(",") { "'$it'" }})", null + ) + } + + override fun deleteSnapshotsSync(snapshotIds: List): Int { + return snapshotDao.deleteSync( + "${ + SnapshotEntity.ColumnNames.ID + } IN (${snapshotIds.joinToString(",") { "'$it'" }})", null) + } + + override fun clearSnapshots() { + snapshotDao.delete(null, null) + } + + override fun resetTillSync(dumpedMetrics: List>) { with(metricDao) { // dbExecutor?.execute { @@ -350,7 +438,6 @@ class DefaultReservoir @JvmOverloads constructor( } override fun getAllMetricsSync(): List> { - return with(metricDao) { val metricEntities = getAllSync() metricEntities?.map { diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturer.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturer.kt new file mode 100644 index 000000000..2418977c9 --- /dev/null +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturer.kt @@ -0,0 +1,76 @@ +/* + * Creator: Debanjan Chatterjee on 06/11/23, 1:05 pm Last modified: 06/11/23, 1:05 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.ruddermetricsreporterandroid.internal + +import android.util.Log +import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCapturer +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCreator +import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModelWithId +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.locks.ReentrantLock + +internal class DefaultSnapshotCapturer( + private val snapshotCreator: SnapshotCreator, + private val snapshotExecutor: ExecutorService = Executors.newCachedThreadPool() +) : SnapshotCapturer { + private val snapshotLock = ReentrantLock() + override fun captureSnapshotsAndResetReservoir( + batchItemCount: Long, reservoir: Reservoir + ): Int { + snapshotLock.lock() + var metrics = reservoir.getMetricsFirstSync(batchItemCount) + var errors = reservoir.getErrorsFirstSync(batchItemCount) + var totalBatches = 0 + var validMetrics = metrics.filterWithValidValues() + while (validMetrics.isNotEmpty() || errors.isNotEmpty()) { + + snapshotCreator.createSnapshot(validMetrics, errors.map { it.errorEvent })?.apply { + if (reservoir.saveSnapshotSync(this) > -1) { + reservoir.resetTillSync(metrics) + reservoir.clearErrorsSync(errors.map { it.id }.toTypedArray()) + } else return totalBatches + + } ?: return totalBatches + totalBatches++ + metrics = if (metrics.size >= batchItemCount) reservoir.getMetricsFirstSync( + batchItemCount * totalBatches, batchItemCount + ) else listOf() + validMetrics = metrics.filterWithValidValues() + errors = reservoir.getErrorsFirstSync(batchItemCount) + } + snapshotLock.unlock() + return totalBatches + } + + override fun captureSnapshotsAndResetReservoir( + batchItemCount: Long, reservoir: Reservoir, callback: (totalBatches: Int) -> Unit + ) { + snapshotExecutor.execute { + callback.invoke(captureSnapshotsAndResetReservoir(batchItemCount, reservoir)) + } + } + + override fun shutdown() { + snapshotExecutor.shutdown() + } + + private fun List>.filterWithValidValues(): List> { + return this.filter { + it.value.toLong() > 0 + } + } +} \ No newline at end of file diff --git a/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreator.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreator.kt new file mode 100644 index 000000000..91f6ca994 --- /dev/null +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreator.kt @@ -0,0 +1,53 @@ +/* + * Creator: Debanjan Chatterjee on 02/11/23, 6:26 pm Last modified: 02/11/23, 6:26 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.ruddermetricsreporterandroid.internal + +import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCreator +import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel +import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import java.util.UUID + +internal class DefaultSnapshotCreator(private val libraryMetadata: LibraryMetadata, + private val apiVersion: Int, private val jsonAdapter: + JsonAdapter): SnapshotCreator { + companion object { + private const val SOURCE_KEY = "source" + private const val METRICS_KEY = "metrics" + private const val ERROR_KEY = "errors" + private const val VERSION_KEY = "version" + private const val MESSAGE_ID_KEY = "message_id" + + } + override fun createSnapshot(metrics: List>, errorEvents: List): + Snapshot? { + val requestMap = HashMap() + requestMap[METRICS_KEY] = metrics + if (errorEvents.isNotEmpty()) + requestMap[ERROR_KEY] = ErrorModel(libraryMetadata, errorEvents).toMap(jsonAdapter) + requestMap[SOURCE_KEY] = libraryMetadata + + requestMap[VERSION_KEY] = apiVersion.toString() + val messageId = UUID.randomUUID().toString() + requestMap[MESSAGE_ID_KEY] = messageId + return jsonAdapter.writeToJson(requestMap, + object: RudderTypeAdapter>() {})?.let { + Snapshot(messageId, it) + } + } +} \ 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..27326944e 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 @@ -14,66 +14,33 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot import com.rudderstack.android.ruddermetricsreporterandroid.UploadMediator -import com.rudderstack.android.ruddermetricsreporterandroid.error.ErrorModel import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ConfigModule -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.rudderjsonadapter.JsonAdapter import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import com.rudderstack.web.WebService import com.rudderstack.web.WebServiceFactory import java.util.concurrent.ExecutorService internal class DefaultUploadMediator( -// dataCollectionModule: DataCollectionModule, - private val configModule: ConfigModule, baseUrl: String, - private val jsonAdapter: JsonAdapter, + jsonAdapter: JsonAdapter, networkExecutor: ExecutorService, - private val apiVersion : Int = 1, - private val isGzipEnabled : Boolean = true + private val isGzipEnabled : Boolean = true, + private val webService: WebService = WebServiceFactory.getWebService(baseUrl, jsonAdapter, + executor = networkExecutor), ) : UploadMediator { // private val deviceDataCollector: DeviceDataCollector - private val webService = WebServiceFactory.getWebService(baseUrl, jsonAdapter, - executor = networkExecutor) - + companion object{ + private const val METRICS_ENDPOINT = "sdkmetrics" + } - 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, + override fun upload(snapshot: Snapshot, callback: (success: Boolean) -> Unit) { + webService.post(null,null, snapshot.snapshot, METRICS_ENDPOINT, object : RudderTypeAdapter>(){}, isGzipEnabled){ (it.status in 200..299).apply(callback) } } - - private fun createRequestMap(metrics: List>, error: ErrorModel): Map { - val requestMap = HashMap() -// requestMap[DEVICE_KEY] = - requestMap[METRICS_KEY] = metrics - requestMap[ERROR_KEY] = error.toMap(jsonAdapter) - requestMap[SOURCE_KEY] = configModule.config.libraryMetadata - - requestMap[VERSION_KEY] = apiVersion.toString() - return requestMap - } -// private fun getSourceJsonFromDeviceAndLibrary(deviceJson: String?, -// libraryMetadataJson: String?): String? { -// return jsonAdapter.writeToJson( -// mapOf( -// DEVICE_KEY to deviceJson, -// LIBRARY_METADATA_KEY to libraryMetadataJson -// ), object : RudderTypeAdapter>(){} -// ) -// } - companion object{ -// private const val DEVICE_KEY = "device" -// private const val LIBRARY_METADATA_KEY = "libraryMetadata" - private const val SOURCE_KEY = "source" - private const val METRICS_KEY = "metrics" - private const val ERROR_KEY = "errors" - 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/models/Snapshot.kt b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/Snapshot.kt new file mode 100644 index 000000000..a4f90ba1a --- /dev/null +++ b/rudderreporter/src/main/java/com/rudderstack/android/ruddermetricsreporterandroid/models/Snapshot.kt @@ -0,0 +1,72 @@ +/* + * Creator: Debanjan Chatterjee on 06/11/23, 9:27 am Last modified: 02/11/23, 8: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.ruddermetricsreporterandroid.models + +import android.content.ContentValues +import com.rudderstack.android.repository.Entity +import com.rudderstack.android.repository.annotation.RudderEntity +import com.rudderstack.android.repository.annotation.RudderField + +data class Snapshot(val id: String, val snapshot: String) + +@RudderEntity( + tableName = SnapshotEntity.TABLE_NAME, [ + RudderField( + RudderField.Type.TEXT, SnapshotEntity.ColumnNames.ID, + primaryKey = true, isNullable = false + ), + RudderField( + RudderField.Type.TEXT, SnapshotEntity.ColumnNames.SNAPSHOT, + primaryKey = false, isNullable = false + ) + ] +) +internal class SnapshotEntity(private val id: String, private val snapshot: String): Entity { + constructor(snapshot: Snapshot): this(snapshot.id, snapshot.snapshot) + object ColumnNames { + const val ID = "id" + const val SNAPSHOT = "snapshot" + } + companion object { + const val TABLE_NAME = "snapshots" + fun create(values: Map): SnapshotEntity{ + val id = values[ColumnNames.ID] as String + val snapshot = values[ColumnNames.SNAPSHOT] as String + return SnapshotEntity(id, snapshot) + } + } + + override fun generateContentValues(): ContentValues { + return ContentValues().apply { + put(ColumnNames.ID, id) + put(ColumnNames.SNAPSHOT, snapshot) + } + } + fun toSnapshot(): Snapshot { + return Snapshot(id, snapshot) + } + override fun getPrimaryKeyValues(): Array { + return arrayOf(id) + } + + override fun equals(other: Any?): Boolean { + return (other is SnapshotEntity) && + id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } +} \ 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..689180ab5 100644 --- a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/LibraryMetadataTest.kt @@ -15,9 +15,6 @@ 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 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..3c39d42a2 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 @@ -31,6 +31,7 @@ import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestExecutor import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.empty import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.everyItem @@ -834,6 +835,123 @@ class DefaultReservoirTest { assertThat(it, equalTo(10L)) } } + @Test + fun `test saveSnapshot`(){ + defaultStorage.clear() + val snapshot = TestDataGenerator.mockSnapshot() + defaultStorage.saveSnapshot(snapshot){ + assertThat(it, Matchers.greaterThan(-1L)) + } + val snapshots = defaultStorage.getAllSnapshotsSync() + println("snaphot: $snapshots") + assertThat(snapshots, Matchers.contains(snapshot)) + + } + @Test + fun `test saveSnapshotSync`(){ + defaultStorage.clear() + val snapshot = TestDataGenerator.mockSnapshot() + val rowId = defaultStorage.saveSnapshotSync(snapshot) + assertThat(rowId, Matchers.greaterThan(-1L)) + + val snapshots = defaultStorage.getAllSnapshotsSync() + println("snaphot: $snapshots") + assertThat(snapshots, Matchers.contains(snapshot)) + + } + @Test + fun `test getAllSnapshotsSync`(){ + + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + + val snapshots = defaultStorage.getAllSnapshotsSync() + assertThat(snapshots, allOf(hasSize(10), Matchers.contains(*snapshotsInput.toTypedArray()))) + } + @Test + fun `test getAllSnapshots`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + + defaultStorage.getAllSnapshots{ + assertThat(it, allOf(hasSize(10), Matchers.contains(*snapshotsInput.toTypedArray()))) + } + } + @Test + fun `test getSnapshots`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + + defaultStorage.getAllSnapshots{ + assertThat(it, allOf(hasSize(10), Matchers.contains(*snapshotsInput.toTypedArray()))) + } + } + @Test + fun `test deleteSnapshots`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + defaultStorage.deleteSnapshots(snapshotsInput.map { it.id }){ + assertThat(it, Matchers.equalTo(10)) + } + val snapshots = defaultStorage.getAllSnapshotsSync() + assertThat(snapshots, Matchers.empty()) + } + @Test + fun `test deleteSnapshots partly`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + defaultStorage.deleteSnapshots(listOf(snapshotsInput.first ().id)){ + assertThat(it, Matchers.equalTo(1)) + } + val snapshots = defaultStorage.getAllSnapshotsSync() + assertThat(snapshots, allOf( Matchers.hasSize (9), not(contains(snapshotsInput.first())))) + } + @Test + fun `test deleteSnapshotsSync`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + val deletedCount = defaultStorage.deleteSnapshotsSync(snapshotsInput.map { it.id }) + assertThat(deletedCount, Matchers.equalTo(10)) + val snapshots = defaultStorage.getAllSnapshotsSync() + assertThat(snapshots, Matchers.empty()) + } + @Test + fun `test deleteSnapshotsSync partly`(){ + defaultStorage.clear() + val snapshotsInput = (1..10).map { TestDataGenerator.mockSnapshot("uuid_$it")} + snapshotsInput.forEach { + val rowId = defaultStorage.saveSnapshotSync(it) + assertThat(rowId, Matchers.greaterThan(-1L)) + } + val deletedCount = defaultStorage.deleteSnapshotsSync(listOf(snapshotsInput.first().id)) + assertThat(deletedCount, Matchers.equalTo(1)) + val snapshots = defaultStorage.getAllSnapshotsSync() + assertThat(snapshots, allOf( Matchers.hasSize (9), not(contains(snapshotsInput.first())))) + } } typealias Labels = Map \ No newline at end of file diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturerTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturerTest.kt new file mode 100644 index 000000000..7d683efb1 --- /dev/null +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCapturerTest.kt @@ -0,0 +1,106 @@ +/* + * Creator: Debanjan Chatterjee on 10/11/23, 10:55 am Last modified: 10/11/23, 10:55 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.ruddermetricsreporterandroid.internal + +import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCreator +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator.getMetricModelWithId +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator.mockSnapshot +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestExecutor +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.kotlin.anyArray +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever + +class DefaultSnapshotCapturerTest { + + private val mockSnapshotCreator = mock() + private val mockReservoir = mock() + private val snapshotCapturer = DefaultSnapshotCapturer(mockSnapshotCreator, TestExecutor()) + + @Test + fun testCaptureSnapshotsAndResetReservoirWithSuccessfulSnapshotCapture() { + val snapshot = mockSnapshot() + val metricModelWithIdList = listOf(getMetricModelWithId(1)) + whenever(mockSnapshotCreator.createSnapshot(anyList(), anyList())).thenReturn(snapshot) + whenever(mockReservoir.getMetricsFirstSync(anyLong())).thenReturn(metricModelWithIdList) + whenever(mockReservoir.getErrorsFirstSync(anyLong())).thenReturn(listOf()) + whenever(mockReservoir.saveSnapshotSync(snapshot)).thenReturn(0L) + + val totalBatches = snapshotCapturer.captureSnapshotsAndResetReservoir(10, mockReservoir) + + assertEquals(1, totalBatches) + verify(mockReservoir).getMetricsFirstSync(10L) + verify(mockReservoir).resetTillSync(metricModelWithIdList) + verify(mockReservoir).clearErrorsSync(emptyArray()) + } + + @Test + fun testCaptureSnapshotsAndResetReservoirWithFailedSnapshotCapture() { + val metricModelWithIdList = listOf(getMetricModelWithId(1)) + + whenever(mockSnapshotCreator.createSnapshot(anyList(), anyList())).thenReturn(null) + whenever(mockReservoir.getMetricsFirstSync(anyLong())).thenReturn(metricModelWithIdList) + whenever(mockReservoir.getErrorsFirstSync(anyLong())).thenReturn(listOf()) + + val totalBatches = snapshotCapturer.captureSnapshotsAndResetReservoir(10, mockReservoir) + + assertEquals(0, totalBatches) + verify(mockReservoir, times(0)).clearErrorsSync(anyArray()) + verify(mockReservoir, times(0)).resetTillSync(anyList()) + } + + @Test + fun testCaptureSnapshotsAndResetReservoirWithConcurrentSnapshotCapture() { + val threads = mutableListOf() + for (i in 1..10) { + val thread = Thread { snapshotCapturer.captureSnapshotsAndResetReservoir(10, mockReservoir) } + threads.add(thread) + thread.start() + } + + for (thread in threads) { + thread.join() + } + + verify(mockReservoir, times(10)).getMetricsFirstSync(10L) + verify(mockReservoir, times(10)).getErrorsFirstSync(anyLong()) + } + + @Test + fun testCaptureSnapshotsAndResetReservoirWithCallbackInvocation() { + val callbackMock = mock<(Int) -> Unit>() + snapshotCapturer.captureSnapshotsAndResetReservoir(10, mockReservoir, callbackMock) + + verify(callbackMock, times(1)).invoke(0) + } + @Test + fun testShutdown() { + val testExecutor = TestExecutor() + val snapshotCapturer = DefaultSnapshotCapturer(mockSnapshotCreator, testExecutor) + + snapshotCapturer.shutdown() + + assertTrue(testExecutor.isTerminated) + } +} \ No newline at end of file diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreatorTest.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreatorTest.kt new file mode 100644 index 000000000..0934ebacc --- /dev/null +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/internal/DefaultSnapshotCreatorTest.kt @@ -0,0 +1,62 @@ +/* + * Creator: Debanjan Chatterjee on 10/11/23, 4:24 pm Last modified: 10/11/23, 4:24 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.ruddermetricsreporterandroid.internal + +import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata +import com.rudderstack.android.ruddermetricsreporterandroid.utils.NotEmptyStringMatcher +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator +import com.rudderstack.rudderjsonadapter.JsonAdapter +import com.rudderstack.rudderjsonadapter.RudderTypeAdapter +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.hasKey +import org.junit.Assert.assertNotNull +import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +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 + +class DefaultSnapshotCreatorTest { + private val mockJsonAdapter = mock() + private val snapshotCreator = DefaultSnapshotCreator( + LibraryMetadata( + "test", "1.0", "1.0", "1.0", "1.0"), 1, mockJsonAdapter) + @Test + fun testCreateSnapshotWithSuccessfulSnapshotCreation() { + val metrics = listOf(TestDataGenerator.getTestMetric(10)) + val errorEvents = listOf("Error event 1") + + whenever(mockJsonAdapter.writeToJson(any(), any())).thenReturn(TestDataGenerator + .mockSnapshot().snapshot) + whenever(mockJsonAdapter.readJson(anyString(), any>>())) + .thenReturn( + mapOf()) + val mapCaptor = argumentCaptor>() + val snapshot = snapshotCreator.createSnapshot(metrics, errorEvents) + verify(mockJsonAdapter).writeToJson(mapCaptor.capture(), any>>()) + + assertNotNull(snapshot) + val mapCaptured = mapCaptor.firstValue.toMap() + println("id captured: ${mapCaptured["message_id"].toString()}") + MatcherAssert.assertThat(mapCaptured, allOf(Matchers.aMapWithSize(5), + hasKey("message_id"), hasKey("metrics"), hasKey("source"), hasKey("errors"), + )) + MatcherAssert.assertThat(mapCaptured["message_id"], NotEmptyStringMatcher()) + } +} \ No newline at end of file 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..d2dc1631b 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 @@ -16,189 +16,177 @@ package com.rudderstack.android.ruddermetricsreporterandroid.internal import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.android.ruddermetricsreporterandroid.Reservoir +import com.rudderstack.android.ruddermetricsreporterandroid.SnapshotCapturer import com.rudderstack.android.ruddermetricsreporterandroid.UploadMediator -import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModel import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricModelWithId import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType import com.rudderstack.android.ruddermetricsreporterandroid.models.ErrorEntity +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator -import org.awaitility.Awaitility -import org.hamcrest.Matchers -import org.hamcrest.Matchers.allOf -import org.hamcrest.Matchers.empty -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.hasSize +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator.mockSnapshot import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.not -import org.hamcrest.Matchers.notNullValue import org.junit.After +import org.junit.Assert.assertFalse import org.junit.Assert.assertThat +import org.junit.Assert.assertTrue import org.junit.Test +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mockito -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger +import org.mockito.Mockito.atMostOnce +import org.mockito.Mockito.mock +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.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever class DefaultSyncerTest { - val mockReservoir = Mockito.mock(Reservoir::class.java) - val mockUploader = Mockito.mock(UploadMediator::class.java) - private val mockLibraryMetadata = LibraryMetadata("test", "1.0.0", "14", "testKey") + val reservoir = Mockito.mock(Reservoir::class.java) + val uploader = Mockito.mock(UploadMediator::class.java) + val snapshotCapturer: SnapshotCapturer = Mockito.mock(SnapshotCapturer::class.java) @Test - fun checkSyncWithSuccess() { - println("********checkSyncWithSuccess***********") - -// var lastMetricsIndex = 0 - val limit = 20 - val maxMetrics = 110 - val maxErrors = 210 - val isMetricsDone = AtomicBoolean(false) - val isErrorsDone = AtomicBoolean(false) - val interval = 200L - mockTheReservoir(maxMetrics, maxErrors) - - mockTheUploaderToSucceed() - val syncer = DefaultSyncer(mockReservoir, mockUploader, mockLibraryMetadata) - var cumulativeIndexMetrics = 0 - var cumulativeIndexMErrors = 0 - syncer.setCallback { uploadedMetrics, uploadedErrorModel, success -> - 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 - isMetricsDone.set(true) - } else { - val expected = getTestMetricList( - cumulativeIndexMetrics, - (maxMetrics - cumulativeIndexMetrics).coerceAtMost(limit) - ) - if (expected.isNotEmpty()) - assertThat( - uploadedMetrics, - allOf( - notNullValue(), - not(empty()), - hasSize((maxMetrics - cumulativeIndexMetrics).coerceAtMost(limit)), - Matchers.contains>( - *(expected.toTypedArray()) - ) - ) - ) - else - assertThat(uploadedMetrics, empty()) - if (cumulativeIndexMErrors < maxErrors) { - val expectedSizeOfErrors = (maxErrors - cumulativeIndexMErrors).coerceAtMost(limit) - assertThat( - uploadedErrorModel.eventsJson, allOf( - notNullValue(), - not(empty()), - hasSize(expectedSizeOfErrors) - ) - ) - }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 - isMetricsDone.set(true) - } - if (cumulativeIndexMErrors + uploadedErrorModel.eventsJson.size == maxErrors) { - isErrorsDone.set(true) - } - if (cumulativeIndexMErrors + uploadedErrorModel.eventsJson.size > maxErrors) { - assert(false) - isErrorsDone.set(true) - } - } - cumulativeIndexMetrics += uploadedMetrics.size - cumulativeIndexMErrors += uploadedErrorModel.eventsJson.size + fun testFlushAllMetricsWithEmptyReservoir() { + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.flushAllMetrics() - } - syncer.startScheduledSyncs(interval, true, limit.toLong()) + verify(reservoir).getSnapshots(1) + verifyNoInteractions(uploader) + verifyNoInteractions(snapshotCapturer) + } - Awaitility.await().atMost(4, TimeUnit.MINUTES).until{ - isMetricsDone.get() && isErrorsDone.get() + @Test + fun testFlushAllMetricsWithSuccessfulUpload() { + val snapshot = mockSnapshot() + whenever(reservoir.getSnapshots(1, 0)).thenReturn(listOf(snapshot)) + whenever(uploader.upload(any(), any())).then { + assertThat(it.arguments[0], `is`(snapshot)) + val callback = it.arguments[1] as ((Boolean) -> Unit) + //for subsequent calls snapshot should be empty + whenever(reservoir.getSnapshots(1, 0)).thenReturn(listOf()) + callback.invoke(true) } - syncer.stopScheduling() - println("********checkSyncWithSuccess***********") + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + + syncer.flushAllMetrics() + verify(uploader).upload(eq(snapshot), any()) + verify(reservoir).deleteSnapshots(eq(listOf(snapshot.id))) } + @Test + fun testFlushAllMetricsWithUploadFailure() { + val snapshot = mockSnapshot() + whenever(reservoir.getSnapshots(1, 0)).thenReturn(listOf(snapshot)) + whenever(uploader.upload(any(), any())).then { + assertThat(it.arguments[0], `is`(snapshot)) + val callback = it.arguments[1] as ((Boolean) -> Unit) + callback.invoke(false) + } + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.flushAllMetrics() + verify(uploader).upload(eq(snapshot), any()) + verify(reservoir, never()).deleteSnapshots(anyList()) + } @Test - fun `test sync with failure`() { - println("********checkSyncWithFailure***********") - - val limit = 20 - val maxMetrics = 110 - val maxErrors = 210 - val interval = 200L - val syncCounter = AtomicInteger(0) - mockTheReservoir(maxMetrics, maxErrors) - - mockTheUploaderToFail() - val syncer = DefaultSyncer(mockReservoir, mockUploader, mockLibraryMetadata) - val expectedMetrics = getTestMetricList( - 0, - (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}") - assertThat(success, `is`(false)) - assertThat(uploadedMetrics,Matchers.contains>( - *(expectedMetrics.toTypedArray()) - ) ) - assertThat( - uploadedErrorModel.eventsJson, allOf( - notNullValue(), - not(empty()), - hasSize(expectedSizeOfErrors) - ) - ) - //let's wait for 5 calls - syncCounter.incrementAndGet() + fun testFlushAllMetricsWithConcurrentCalls() { + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + + // Simulate concurrent calls by creating multiple threads + val threads = mutableListOf() + for (i in 1..10) { + val thread = Thread { syncer.flushAllMetrics() } + threads.add(thread) + thread.start() } - syncer.startScheduledSyncs(interval, true, limit.toLong()) - Awaitility.await().atMost(2, TimeUnit.MINUTES).untilAtomic(syncCounter, equalTo(5)) - syncer.stopScheduling() - println("********checkSyncWithFailure***********") + for (thread in threads) { + thread.join() + } + // Verify that only one thread was actively flushing snapshots at a time + verify(reservoir, atMostOnce()).getMetricsFirstSync(anyLong()) + verify(uploader, atMostOnce()).upload(any(), any()) + verify(snapshotCapturer, atMostOnce()).captureSnapshotsAndResetReservoir(anyLong(), any(), any()) } + @Test + fun testFlushAllMetricsWithSnapshotCaptureFailure() { + whenever(snapshotCapturer.captureSnapshotsAndResetReservoir(anyLong(), any(), any())).then{ + val callback = it.arguments[2] as ((Int) -> Unit) + callback.invoke(0) + } + whenever(reservoir.getSnapshots(1)).thenReturn(listOf()) + + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.flushAllMetrics() + verify(snapshotCapturer).captureSnapshotsAndResetReservoir(anyLong(), any(), any()) + verifyNoInteractions(uploader) + } @Test - fun stopScheduling() { - val limit = 20 - val maxMetrics = 110 - val maxErrors = 210 - val interval = 200L - mockTheReservoir(maxMetrics, maxErrors) - - mockTheUploaderToSucceed() - val syncer = DefaultSyncer(mockReservoir, mockUploader, mockLibraryMetadata) - syncer.startScheduledSyncs(interval, true, limit.toLong()) - Thread.sleep(interval/2) //some time elapse before stopping + fun testStopScheduling() { + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.stopScheduling() - //waiting to stop - Thread.sleep(interval + 10) - syncer.setCallback{ _, _, _ -> - assert(false) //call shouldn't reach here + verify(snapshotCapturer).shutdown() + } + @Test + fun testCallbackAfterSuccessfulUpload() { + val snapshot = mockSnapshot() + val callback: (Snapshot, Boolean) -> Unit = mock() + whenever(snapshotCapturer.captureSnapshotsAndResetReservoir(anyLong(), any(), any())).then{ + val callback = it.arguments[2] as ((Int) -> Unit) + callback.invoke(0) } - Thread.sleep(interval*5) + whenever(reservoir.getSnapshots(1)).thenReturn(listOf(snapshot)) + whenever(uploader.upload(any(), any())).then { + assertThat(it.arguments[0], `is`(snapshot)) + val callback = it.arguments[1] as ((Boolean) -> Unit) + whenever(reservoir.getSnapshots(1)).thenReturn(listOf()) + + callback.invoke(true) + } + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.setCallback(callback) + + syncer.flushAllMetrics() + + verify(callback, times(1)).invoke(snapshot, true) } + @Test + fun testCallbackAfterFailedlUpload() { + val snapshot = mockSnapshot() + val callback: (Snapshot, Boolean) -> Unit = mock() + whenever(snapshotCapturer.captureSnapshotsAndResetReservoir(anyLong(), any(), any())).then{ + val callback = it.arguments[2] as ((Int) -> Unit) + callback.invoke(0) + } + whenever(reservoir.getSnapshots(1)).thenReturn(listOf(snapshot)) + whenever(uploader.upload(any(), any())).then { + assertThat(it.arguments[0], `is`(snapshot)) + val callback = it.arguments[1] as ((Boolean) -> Unit) + whenever(reservoir.getSnapshots(1)).thenReturn(listOf()) + + callback.invoke(false) + } + val syncer = DefaultPeriodicSyncer(reservoir, uploader, snapshotCapturer) + syncer.setCallback(callback) + syncer.flushAllMetrics() + + verify(callback, times(1)).invoke(snapshot, false) + } @Test fun testTimerWithCallbackOnStart() { - val scheduler = DefaultSyncer.Scheduler() + val scheduler = DefaultPeriodicSyncer.Scheduler() var schedulerCalled = 0 scheduler.scheduleTimer(true, 500L) { println("timer called") @@ -212,7 +200,7 @@ class DefaultSyncerTest { @Test fun testTimerWithNoCallbackOnStart() { - val scheduler = DefaultSyncer.Scheduler() + val scheduler = DefaultPeriodicSyncer.Scheduler() var schedulerCalled = 0 scheduler.scheduleTimer(false, 500L) { println("timer called") @@ -226,7 +214,7 @@ class DefaultSyncerTest { @Test fun testTimerStoppedDuringExec() { - val scheduler = DefaultSyncer.Scheduler() + val scheduler = DefaultPeriodicSyncer.Scheduler() var schedulerCalled = 0 scheduler.scheduleTimer(true, 500L) { @@ -241,73 +229,7 @@ class DefaultSyncerTest { assertThat(schedulerCalled, `is`(3)) } - private fun getTestMetricList(startPos: Int, limit: Int): List> { - - return (startPos until startPos + limit).map { - val index = it + 1 - MetricModelWithId( - (index).toString(), - "testMetric$it", - MetricType.COUNTER, - index.toLong(), - 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() - ) - ).then { - val callback = it.arguments[2] as ((Boolean) -> Unit) - callback.invoke(true) - } - } - 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() - ) - ).then { - 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() - // 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) - }) - } - Mockito.`when`(mockReservoir.clearErrors(org.mockito.kotlin.any())).then { - val idsToCLear = it.arguments[0] as Array - errorBeginIndex += idsToCLear.size - Unit - } - } - private fun mockTheUploaderToFail() { - Mockito.`when`( - mockUploader.upload( - 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..7c9541cd5 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 @@ -21,34 +21,65 @@ import com.rudderstack.android.ruddermetricsreporterandroid.Configuration import com.rudderstack.android.ruddermetricsreporterandroid.LibraryMetadata import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ConfigModule import com.rudderstack.android.ruddermetricsreporterandroid.internal.di.ContextModule +import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestDataGenerator import com.rudderstack.android.ruddermetricsreporterandroid.utils.TestExecutor 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.rudderstack.web.HttpInterceptor +import com.rudderstack.web.HttpResponse +import com.rudderstack.web.WebService import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Suite +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.annotation.Config import java.util.Date +import java.util.concurrent.Future + @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() - ) + @Test + fun `test uploadWithSuccessfulUpload`() { + val mockSnapshot = TestDataGenerator.mockSnapshot() + val callbackMock = mock<(Boolean) -> Unit>() + val mockWebService = MockPostWebService(200) + val uploadMediator = DefaultUploadMediator("https://api.example.com", mock(), mock(), true, mockWebService) + + uploadMediator.upload(mockSnapshot, callbackMock) + + verify(callbackMock, times(1)).invoke(true) + } @Test - fun upload() { - //TODO: add test for upload + fun `test uploadWithFailedUpload`() { + val mockSnapshot = TestDataGenerator.mockSnapshot() + val callbackMock = mock<(Boolean) -> Unit>() + val mockWebService = MockPostWebService(400) + val uploadMediator = DefaultUploadMediator("https://api.example.com", mock(), mock(), true, mockWebService) + + + uploadMediator.upload(mockSnapshot, callbackMock) + + verify(callbackMock, times(1)).invoke(false) } + + companion object { private const val MANUFACTURER = "Google" private const val MODEL = "pixel 7" @@ -58,31 +89,99 @@ open class DefaultUploaderTest { private const val ID = "id" } -} -class DefaultUploaderTestGson : DefaultUploaderTest() { - init { - jsonAdapter = GsonAdapter() - } -} + class MockPostWebService(private val statusCode: Int) : WebService { + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseClass: Class + ): Future> { + TODO("Not yet implemented") + } -class DefaultUploaderTestJackson : DefaultUploaderTest() { - init { - jsonAdapter = JacksonAdapter() - } -} + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter + ): Future> { + TODO("Not yet implemented") + } + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + callback: (HttpResponse) -> Unit + ) { + TODO("Not yet implemented") + } + + override fun get( + headers: Map?, + query: Map?, + endpoint: String, + responseClass: Class, + callback: (HttpResponse) -> Unit + ) { + TODO("Not yet implemented") + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseClass: Class, + isGzipEnabled: Boolean + ): Future> { + TODO("Not yet implemented") + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + isGzipEnabled: Boolean + ): Future> { + TODO("Not yet implemented") + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseClass: Class, + isGzipEnabled: Boolean, + callback: (HttpResponse) -> Unit + ) { + callback.invoke(HttpResponse(statusCode, null, null)) + } + + override fun post( + headers: Map?, + query: Map?, + body: String?, + endpoint: String, + responseTypeAdapter: RudderTypeAdapter, + isGzipEnabled: Boolean, + callback: (HttpResponse) -> Unit + ) { + callback.invoke(HttpResponse(statusCode, null, null)) + } + + override fun setInterceptor(httpInterceptor: HttpInterceptor) { + TODO("Not yet implemented") + } -class DefaultUploaderTestMoshi : DefaultUploaderTest() { - init { - jsonAdapter = MoshiAdapter() + override fun shutdown(shutdownExecutor: Boolean) { + TODO("Not yet implemented") + } } } -@RunWith(Suite::class) -@Suite.SuiteClasses( - DefaultUploaderTestGson::class, - DefaultUploaderTestJackson::class, - DefaultUploaderTestMoshi::class -) -class DefaultUploaderTestSuite { -} \ No newline at end of file diff --git a/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/NotEmptyStringMatcher.kt b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/NotEmptyStringMatcher.kt new file mode 100644 index 000000000..ffcfb33a7 --- /dev/null +++ b/rudderreporter/src/test/java/com/rudderstack/android/ruddermetricsreporterandroid/utils/NotEmptyStringMatcher.kt @@ -0,0 +1,28 @@ +/* + * Creator: Debanjan Chatterjee on 13/11/23, 12:21 pm Last modified: 13/11/23, 12:21 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.ruddermetricsreporterandroid.utils + +import org.hamcrest.BaseMatcher +import org.hamcrest.Description + +class NotEmptyStringMatcher : BaseMatcher() { + override fun describeTo(description: Description?) { + description?.appendText("Not empty string") + } + + override fun matches(item: Any?): Boolean { + return (item is String && item.isNotEmpty()) || (item != null && item.toString() != "") + } +} \ 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..5f3d06d06 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 @@ -16,7 +16,9 @@ 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.MetricModelWithId import com.rudderstack.android.ruddermetricsreporterandroid.metrics.MetricType +import com.rudderstack.android.ruddermetricsreporterandroid.models.Snapshot object TestDataGenerator { @@ -32,6 +34,15 @@ object TestDataGenerator { getTestErrorEventJsonWithIdentity(it) } + fun generateMetricModelWithId(count : Int) = (1..count).map { + getMetricModelWithId(it) + } + + fun getMetricModelWithId(id: Int) = + MetricModelWithId(id.toString(),"test_metric_$id", + MetricType.COUNTER, id.toLong(), mapOf("type" to "type_$id")) + + fun generateTestErrorEventsJson(range: Iterable) = range.map { getTestErrorEventJsonWithIdentity(it) @@ -299,4 +310,1930 @@ object TestDataGenerator { } """ + fun mockSnapshot(id: String = "3cc8e4a4-71ca-491e-a5eb-aef83a4b0489") = Snapshot(id, + """ + { + "message_id": "$id", + "metrics": [ + + ], + "source": { + "name": "com.rudderstack.android.sdk.core", + "os_version": "29", + "sdk_version": "1.20.1", + "version_code": "20", + "write_key": "1xXCubSHWXbpBI2h6EpCjKOsxmQ" + }, + "version": "1", + "errors": { + "events": [ + { + "exceptions": [ + { + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023", + "stacktrace": [ + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity + .onCreate${"$"}lambda\${"$"}{'$'}5", + "file": "MainActivity.kt", + "lineNumber": 78.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.${"$"}r8${"$"}lambda${"$"}{'$'}0jx5-_ODOzEyJmgtXRqjTRGD--8", + "file": "MainActivity.kt", + "lineNumber": 0.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity${'$'}${"$"}ExternalSyntheticLambda5.onClick", + "file": "R8${'$'}${"$"}SyntheticClass", + "lineNumber": 0.0 + }, + { + "method": "android.view.View.performClick", + "file": "View.java", + "lineNumber": 7125.0 + }, + { + "method": "android.view.View.performClickInternal", + "file": "View.java", + "lineNumber": 7102.0 + }, + { + "method": "android.view.View.access${'$'}3500", + "file": "View.java", + "lineNumber": 801.0 + }, + { + "method": "android.view.View${"$"}PerformClick.run", + "file": "View.java", + "lineNumber": 27336.0 + }, + { + "method": "android.os.Handler.handleCallback", + "file": "Handler.java", + "lineNumber": 883.0 + }, + { + "method": "android.os.Handler.dispatchMessage", + "file": "Handler.java", + "lineNumber": 100.0 + }, + { + "method": "android.os.Looper.loop", + "file": "Looper.java", + "lineNumber": 214.0 + }, + { + "method": "android.app.ActivityThread.main", + "file": "ActivityThread.java", + "lineNumber": 7356.0 + }, + { + "method": "java.lang.reflect.Method.invoke", + "file": "Method.java", + "lineNumber": -2.0 + }, + { + "method": "com.android.internal.os.RuntimeInit${"$"}MethodAndArgsCaller.run", + "file": "RuntimeInit.java", + "lineNumber": 492.0 + }, + { + "method": "com.android.internal.os.ZygoteInit.main", + "file": "ZygoteInit.java", + "lineNumber": 930.0 + } + ], + "type": "ANDROID" + } + ], + "severity": "WARNING", + "breadcrumbs": [ + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.020Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.160Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.329Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.478Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.626Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.774Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.928Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.099Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.242Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.393Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.545Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.693Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.859Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.045Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.177Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.539Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.691Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.862Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.010Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.178Z", + "type": "ERROR" + } + ], + "unhandled": false, + "projectPackages": [ + "com.example.testapp1mg" + ], + "app": { + "id": "com.example.testapp1mg", + "releaseStage": "development", + "version": "1.20.1", + "versionCode": "20" + }, + "device": { + "manufacturer": "Google", + "model": "Android SDK built for x86", + "osName": "android", + "osVersion": "10", + "cpuAbi": "x86", + "jailbroken": "true", + "locale": "en_US", + "totalMemory": "2089168896", + "runtimeVersions": { + "androidApiLevel": "29", + "osBuild": "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" + }, + "freeDisk": "1690099712", + "freeMemory": "942084096", + "orientation": "portrait", + "time": "2023-11-08T19:23:55.304Z" + }, + "metadata": { + "app": { + "memoryUsage": 4672792.0, + "memoryTrimLevel": "None", + "totalMemory": 6594415.0, + "processName": "com.example.testapp1mg", + "name": "Sample Kotlin", + "memoryLimit": 5.36870912E8, + "lowMemory": false, + "freeMemory": 1921623.0 + }, + "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.0, + "screenResolution": "1776x1080", + "brand": "google", + "apiLevel": 29.0, + "batteryLevel": 1.0, + "cpuAbis": [ + "x86" + ], + "charging": false, + "tags": "dev-keys", + "emulator": true, + "screenDensity": 3.0 + } + } + }, + { + "exceptions": [ + { + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023", + "stacktrace": [ + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.onCreate${"$"}lambda${'$'}5", + "file": "MainActivity.kt", + "lineNumber": 78.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.${"$"}r8${"$"}lambda${'$'}0jx5-_ODOzEyJmgtXRqjTRGD--8", + "file": "MainActivity.kt", + "lineNumber": 0.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity${'$'}${"$"}ExternalSyntheticLambda5.onClick", + "file": "R8${'$'}${"$"}SyntheticClass", + "lineNumber": 0.0 + }, + { + "method": "android.view.View.performClick", + "file": "View.java", + "lineNumber": 7125.0 + }, + { + "method": "android.view.View.performClickInternal", + "file": "View.java", + "lineNumber": 7102.0 + }, + { + "method": "android.view.View.access${'$'}3500", + "file": "View.java", + "lineNumber": 801.0 + }, + { + "method": "android.view.View${"$"}PerformClick.run", + "file": "View.java", + "lineNumber": 27336.0 + }, + { + "method": "android.os.Handler.handleCallback", + "file": "Handler.java", + "lineNumber": 883.0 + }, + { + "method": "android.os.Handler.dispatchMessage", + "file": "Handler.java", + "lineNumber": 100.0 + }, + { + "method": "android.os.Looper.loop", + "file": "Looper.java", + "lineNumber": 214.0 + }, + { + "method": "android.app.ActivityThread.main", + "file": "ActivityThread.java", + "lineNumber": 7356.0 + }, + { + "method": "java.lang.reflect.Method.invoke", + "file": "Method.java", + "lineNumber": -2.0 + }, + { + "method": "com.android.internal.os.RuntimeInit${"$"}MethodAndArgsCaller.run", + "file": "RuntimeInit.java", + "lineNumber": 492.0 + }, + { + "method": "com.android.internal.os.ZygoteInit.main", + "file": "ZygoteInit.java", + "lineNumber": 930.0 + } + ], + "type": "ANDROID" + } + ], + "severity": "WARNING", + "breadcrumbs": [ + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.020Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.160Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.329Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.478Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.626Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.774Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.928Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.099Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.242Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.393Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.545Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.693Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.859Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.045Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.177Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.539Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.691Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.862Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.010Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.178Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.310Z", + "type": "ERROR" + } + ], + "unhandled": false, + "projectPackages": [ + "com.example.testapp1mg" + ], + "app": { + "id": "com.example.testapp1mg", + "releaseStage": "development", + "version": "1.20.1", + "versionCode": "20" + }, + "device": { + "manufacturer": "Google", + "model": "Android SDK built for x86", + "osName": "android", + "osVersion": "10", + "cpuAbi": "x86", + "jailbroken": "true", + "locale": "en_US", + "totalMemory": "2089168896", + "runtimeVersions": { + "androidApiLevel": "29", + "osBuild": "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" + }, + "freeDisk": "1690091520", + "freeMemory": "942030848", + "orientation": "portrait", + "time": "2023-11-08T19:23:55.437Z" + }, + "metadata": { + "app": { + "memoryUsage": 4792008.0, + "memoryTrimLevel": "None", + "totalMemory": 6594415.0, + "processName": "com.example.testapp1mg", + "name": "Sample Kotlin", + "memoryLimit": 5.36870912E8, + "lowMemory": false, + "freeMemory": 1802407.0 + }, + "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.0, + "screenResolution": "1776x1080", + "brand": "google", + "apiLevel": 29.0, + "batteryLevel": 1.0, + "cpuAbis": [ + "x86" + ], + "charging": false, + "tags": "dev-keys", + "emulator": true, + "screenDensity": 3.0 + } + } + }, + { + "exceptions": [ + { + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023", + "stacktrace": [ + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.onCreate${"$"}lambda${'$'}5", + "file": "MainActivity.kt", + "lineNumber": 78.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.${"$"}r8${"$"}lambda${"$"}{'$'}0jx5-_ODOzEyJmgtXRqjTRGD--8", + "file": "MainActivity.kt", + "lineNumber": 0.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity${'$'}${"$"}ExternalSyntheticLambda5.onClick", + "file": "R8${'$'}${"$"}SyntheticClass", + "lineNumber": 0.0 + }, + { + "method": "android.view.View.performClick", + "file": "View.java", + "lineNumber": 7125.0 + }, + { + "method": "android.view.View.performClickInternal", + "file": "View.java", + "lineNumber": 7102.0 + }, + { + "method": "android.view.View.access${'$'}3500", + "file": "View.java", + "lineNumber": 801.0 + }, + { + "method": "android.view.View${"$"}PerformClick.run", + "file": "View.java", + "lineNumber": 27336.0 + }, + { + "method": "android.os.Handler.handleCallback", + "file": "Handler.java", + "lineNumber": 883.0 + }, + { + "method": "android.os.Handler.dispatchMessage", + "file": "Handler.java", + "lineNumber": 100.0 + }, + { + "method": "android.os.Looper.loop", + "file": "Looper.java", + "lineNumber": 214.0 + }, + { + "method": "android.app.ActivityThread.main", + "file": "ActivityThread.java", + "lineNumber": 7356.0 + }, + { + "method": "java.lang.reflect.Method.invoke", + "file": "Method.java", + "lineNumber": -2.0 + }, + { + "method": "com.android.internal.os.RuntimeInit${"$"}MethodAndArgsCaller.run", + "file": "RuntimeInit.java", + "lineNumber": 492.0 + }, + { + "method": "com.android.internal.os.ZygoteInit.main", + "file": "ZygoteInit.java", + "lineNumber": 930.0 + } + ], + "type": "ANDROID" + } + ], + "severity": "WARNING", + "breadcrumbs": [ + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.020Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.160Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.329Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.478Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.626Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.774Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.928Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.099Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.242Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.393Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.545Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.693Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.859Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.045Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.177Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.539Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.691Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.862Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.010Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.178Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.310Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.443Z", + "type": "ERROR" + } + ], + "unhandled": false, + "projectPackages": [ + "com.example.testapp1mg" + ], + "app": { + "id": "com.example.testapp1mg", + "releaseStage": "development", + "version": "1.20.1", + "versionCode": "20" + }, + "device": { + "manufacturer": "Google", + "model": "Android SDK built for x86", + "osName": "android", + "osVersion": "10", + "cpuAbi": "x86", + "jailbroken": "true", + "locale": "en_US", + "totalMemory": "2089168896", + "runtimeVersions": { + "androidApiLevel": "29", + "osBuild": "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" + }, + "freeDisk": "1690083328", + "freeMemory": "941907968", + "orientation": "portrait", + "time": "2023-11-08T19:23:55.585Z" + }, + "metadata": { + "app": { + "memoryUsage": 4943992.0, + "memoryTrimLevel": "None", + "totalMemory": 6594415.0, + "processName": "com.example.testapp1mg", + "name": "Sample Kotlin", + "memoryLimit": 5.36870912E8, + "lowMemory": false, + "freeMemory": 1650423.0 + }, + "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.0, + "screenResolution": "1776x1080", + "brand": "google", + "apiLevel": 29.0, + "batteryLevel": 1.0, + "cpuAbis": [ + "x86" + ], + "charging": false, + "tags": "dev-keys", + "emulator": true, + "screenDensity": 3.0 + } + } + }, + { + "exceptions": [ + { + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023", + "stacktrace": [ + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.onCreate${"$"}lambda${'$'}5", + "file": "MainActivity.kt", + "lineNumber": 78.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.${"$"}r8${"$"}lambda${'$'}0jx5-_ODOzEyJmgtXRqjTRGD--8", + "file": "MainActivity.kt", + "lineNumber": 0.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity${'$'}${"$"}ExternalSyntheticLambda5.onClick", + "file": "R8${'$'}${"$"}SyntheticClass", + "lineNumber": 0.0 + }, + { + "method": "android.view.View.performClick", + "file": "View.java", + "lineNumber": 7125.0 + }, + { + "method": "android.view.View.performClickInternal", + "file": "View.java", + "lineNumber": 7102.0 + }, + { + "method": "android.view.View.access${'$'}3500", + "file": "View.java", + "lineNumber": 801.0 + }, + { + "method": "android.view.View${"$"}PerformClick.run", + "file": "View.java", + "lineNumber": 27336.0 + }, + { + "method": "android.os.Handler.handleCallback", + "file": "Handler.java", + "lineNumber": 883.0 + }, + { + "method": "android.os.Handler.dispatchMessage", + "file": "Handler.java", + "lineNumber": 100.0 + }, + { + "method": "android.os.Looper.loop", + "file": "Looper.java", + "lineNumber": 214.0 + }, + { + "method": "android.app.ActivityThread.main", + "file": "ActivityThread.java", + "lineNumber": 7356.0 + }, + { + "method": "java.lang.reflect.Method.invoke", + "file": "Method.java", + "lineNumber": -2.0 + }, + { + "method": "com.android.internal.os.RuntimeInit${"$"}MethodAndArgsCaller.run", + "file": "RuntimeInit.java", + "lineNumber": 492.0 + }, + { + "method": "com.android.internal.os.ZygoteInit.main", + "file": "ZygoteInit.java", + "lineNumber": 930.0 + } + ], + "type": "ANDROID" + } + ], + "severity": "WARNING", + "breadcrumbs": [ + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.020Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.160Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.329Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.478Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.626Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.774Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.928Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.099Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.242Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.393Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.545Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.693Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.859Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.045Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.177Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.539Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.691Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.862Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.010Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.178Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.310Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.443Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.589Z", + "type": "ERROR" + } + ], + "unhandled": false, + "projectPackages": [ + "com.example.testapp1mg" + ], + "app": { + "id": "com.example.testapp1mg", + "releaseStage": "development", + "version": "1.20.1", + "versionCode": "20" + }, + "device": { + "manufacturer": "Google", + "model": "Android SDK built for x86", + "osName": "android", + "osVersion": "10", + "cpuAbi": "x86", + "jailbroken": "true", + "locale": "en_US", + "totalMemory": "2089168896", + "runtimeVersions": { + "androidApiLevel": "29", + "osBuild": "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" + }, + "freeDisk": "1690075136", + "freeMemory": "941797376", + "orientation": "portrait", + "time": "2023-11-08T19:23:55.737Z" + }, + "metadata": { + "app": { + "memoryUsage": 5063208.0, + "memoryTrimLevel": "None", + "totalMemory": 6594415.0, + "processName": "com.example.testapp1mg", + "name": "Sample Kotlin", + "memoryLimit": 5.36870912E8, + "lowMemory": false, + "freeMemory": 1531207.0 + }, + "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.0, + "screenResolution": "1776x1080", + "brand": "google", + "apiLevel": 29.0, + "batteryLevel": 1.0, + "cpuAbis": [ + "x86" + ], + "charging": false, + "tags": "dev-keys", + "emulator": true, + "screenDensity": 3.0 + } + } + } + ], + "payloadVersion": 5, + "notifier": { + "name": "com.rudderstack.android.sdk.core", + "version": "1.20.1", + "url": "https://github.com/rudderlabs/rudder-sdk-android", + "os_version": "29" + } + } + } + + """.trimIndent()) + + + val exceptionJson = """ + { + "exceptions": [ + { + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023", + "stacktrace": [ + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity + .onCreate${"$"}lambda\${'$'}{"${'$'}"}{'${'$'}'}5", + "file": "MainActivity.kt", + "lineNumber": 78.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity.${"$"}r8${"$"}lambda${"$"}{'${'$'}'}0jx5-_ODOzEyJmgtXRqjTRGD--8", + "file": "MainActivity.kt", + "lineNumber": 0.0 + }, + { + "method": "com.rudderstack.android.sample.kotlin.MainActivity${'$'}${"$"}ExternalSyntheticLambda5.onClick", + "file": "R8${'$'}${"$"}SyntheticClass", + "lineNumber": 0.0 + }, + { + "method": "android.view.View.performClick", + "file": "View.java", + "lineNumber": 7125.0 + }, + { + "method": "android.view.View.performClickInternal", + "file": "View.java", + "lineNumber": 7102.0 + }, + { + "method": "android.view.View.access${'$'}3500", + "file": "View.java", + "lineNumber": 801.0 + }, + { + "method": "android.view.View${"$"}PerformClick.run", + "file": "View.java", + "lineNumber": 27336.0 + }, + { + "method": "android.os.Handler.handleCallback", + "file": "Handler.java", + "lineNumber": 883.0 + }, + { + "method": "android.os.Handler.dispatchMessage", + "file": "Handler.java", + "lineNumber": 100.0 + }, + { + "method": "android.os.Looper.loop", + "file": "Looper.java", + "lineNumber": 214.0 + }, + { + "method": "android.app.ActivityThread.main", + "file": "ActivityThread.java", + "lineNumber": 7356.0 + }, + { + "method": "java.lang.reflect.Method.invoke", + "file": "Method.java", + "lineNumber": -2.0 + }, + { + "method": "com.android.internal.os.RuntimeInit${"$"}MethodAndArgsCaller.run", + "file": "RuntimeInit.java", + "lineNumber": 492.0 + }, + { + "method": "com.android.internal.os.ZygoteInit.main", + "file": "ZygoteInit.java", + "lineNumber": 930.0 + } + ], + "type": "ANDROID" + } + ], + "severity": "WARNING", + "breadcrumbs": [ + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.020Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.160Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.329Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.478Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.626Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.774Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:41 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:41.928Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.099Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.242Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.393Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.545Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.693Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:42 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:42.859Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.045Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:43 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:43.177Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.539Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.691Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:54 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:54.862Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.010Z", + "type": "ERROR" + }, + { + "metadata": { + "unhandled": "false", + "severity": "WARNING", + "errorClass": "java.lang.Exception", + "message": "Test Error-Thu Nov 09 00:53:55 GMT+05:30 2023" + }, + "name": "java.lang.Exception", + "timestamp": "2023-11-08T19:23:55.178Z", + "type": "ERROR" + } + ], + "unhandled": false, + "projectPackages": [ + "com.example.testapp1mg" + ], + "app": { + "id": "com.example.testapp1mg", + "releaseStage": "development", + "version": "1.20.1", + "versionCode": "20" + }, + "device": { + "manufacturer": "Google", + "model": "Android SDK built for x86", + "osName": "android", + "osVersion": "10", + "cpuAbi": "x86", + "jailbroken": "true", + "locale": "en_US", + "totalMemory": "2089168896", + "runtimeVersions": { + "androidApiLevel": "29", + "osBuild": "sdk_gphone_x86-userdebug 10 QSR1.210802.001 7603624 dev-keys" + }, + "freeDisk": "1690099712", + "freeMemory": "942084096", + "orientation": "portrait", + "time": "2023-11-08T19:23:55.304Z" + }, + "metadata": { + "app": { + "memoryUsage": 4672792.0, + "memoryTrimLevel": "None", + "totalMemory": 6594415.0, + "processName": "com.example.testapp1mg", + "name": "Sample Kotlin", + "memoryLimit": 5.36870912E8, + "lowMemory": false, + "freeMemory": 1921623.0 + }, + "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.0, + "screenResolution": "1776x1080", + "brand": "google", + "apiLevel": 29.0, + "batteryLevel": 1.0, + "cpuAbis": [ + "x86" + ], + "charging": false, + "tags": "dev-keys", + "emulator": true, + "screenDensity": 3.0 + } + } + } + """.trimIndent() } \ No newline at end of file diff --git a/web/build.gradle b/web/build.gradle index 87e39b4ad..211f4db34 100644 --- a/web/build.gradle +++ b/web/build.gradle @@ -50,6 +50,6 @@ dependencies { testImplementation deps.mockito testImplementation deps.awaitility } -apply from: rootProject.file('gradle/artifacts-jar.gradle') -apply from: rootProject.file('gradle/mvn-publish.gradle') -apply from: rootProject.file('gradle/codecov.gradle') +apply from: "$projectDir/../gradle/artifacts-jar.gradle" +apply from: "$projectDir/../gradle/mvn-publish.gradle" +apply from: "$projectDir/../gradle/codecov.gradle" \ No newline at end of file