Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
680 changes: 680 additions & 0 deletions app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.duckduckgo.app.CoroutineTestRule
import com.duckduckgo.app.blockingObserve
import com.duckduckgo.app.global.exception.UncaughtExceptionEntity
import com.duckduckgo.app.global.exception.UncaughtExceptionSource
import com.duckduckgo.app.onboarding.store.AppStage
import com.duckduckgo.app.runBlocking
import com.nhaarman.mockitokotlin2.any
Expand Down Expand Up @@ -196,6 +198,13 @@ class AppDatabaseTest {
assertEquals(AppStage.ESTABLISHED, database().userStageDao().currentUserAppStage()?.appStage)
}

@Test
fun whenMigratingFromVersion18To19ThenValidationSucceedsAndRowsDeletedFromTable() {
database().uncaughtExceptionDao().add(UncaughtExceptionEntity(1, UncaughtExceptionSource.GLOBAL, "version", 1234))
createDatabaseAndMigrate(18, 19, migrationsProvider.MIGRATION_18_TO_19)
assertEquals(0, database().uncaughtExceptionDao().count())
}

private fun createDatabase(version: Int) {
testHelper.createDatabase(TEST_DB_NAME, version).close()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class UncaughtExceptionDaoTest {
assertEquals(1, id)
assertEquals(exception.exceptionSource, exceptionSource)
assertEquals(exception.message, message)
assertEquals(exception.version, version)
assertEquals(exception.timestamp, timestamp)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2020 DuckDuckGo
*
* 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.duckduckgo.app.statistics.api

import com.duckduckgo.app.CoroutineTestRule
import com.duckduckgo.app.InstantSchedulersRule
import com.duckduckgo.app.global.exception.UncaughtExceptionEntity
import com.duckduckgo.app.global.exception.UncaughtExceptionRepository
import com.duckduckgo.app.global.exception.UncaughtExceptionSource
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_APP_VERSION
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_MESSAGE
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_TIMESTAMP
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
import com.nhaarman.mockitokotlin2.*
import io.reactivex.Completable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@ExperimentalCoroutinesApi
class OfflinePixelSenderTest {

private var mockOfflinePixelCountDataStore: OfflinePixelCountDataStore = mock()
private var mockUncaughtExceptionRepository: UncaughtExceptionRepository = mock()
private var mockPixel: Pixel = mock()

private var testee: OfflinePixelSender = OfflinePixelSender(mockOfflinePixelCountDataStore, mockUncaughtExceptionRepository, mockPixel)

@get:Rule
val schedulers = InstantSchedulersRule()

@ExperimentalCoroutinesApi
@get:Rule
var coroutineRule = CoroutineTestRule()

@Before
fun before() {
val exceptionEntity = UncaughtExceptionEntity(1, UncaughtExceptionSource.GLOBAL, "test", 1588167165000, "version")

runBlocking<Unit> {
whenever(mockPixel.fireCompletable(any(), any(), any())).thenReturn(Completable.complete())
whenever(mockUncaughtExceptionRepository.getExceptions()).thenReturn(listOf(exceptionEntity))
}
}

@Test
fun whenSendUncaughtExceptionsPixelThenTimestampFormattedToUtc() {
val params = mapOf(
EXCEPTION_MESSAGE to "test",
EXCEPTION_APP_VERSION to "version",
EXCEPTION_TIMESTAMP to "2020-04-29T13:32:45+0000"
)

testee.sendOfflinePixels().blockingAwait()

verify(mockPixel).fireCompletable(Pixel.PixelName.APPLICATION_CRASH_GLOBAL.pixelName, params)
}
}
12 changes: 10 additions & 2 deletions app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao
import com.duckduckgo.app.usage.search.SearchCountEntity

@Database(
exportSchema = true, version = 18, entities = [
exportSchema = true, version = 19, entities = [
TdsTracker::class,
TdsEntity::class,
TdsDomainEntity::class,
Expand Down Expand Up @@ -269,6 +269,13 @@ class MigrationsProvider(val context: Context) {
}
}

val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `UncaughtExceptionEntity`")
database.execSQL("CREATE TABLE IF NOT EXISTS `UncaughtExceptionEntity` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL, `version` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)")
}
}

val ALL_MIGRATIONS: List<Migration>
get() = listOf(
MIGRATION_1_TO_2,
Expand All @@ -287,7 +294,8 @@ class MigrationsProvider(val context: Context) {
MIGRATION_14_TO_15,
MIGRATION_15_TO_16,
MIGRATION_16_TO_17,
MIGRATION_17_TO_18
MIGRATION_17_TO_18,
MIGRATION_18_TO_19
)

@Deprecated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,24 @@ package com.duckduckgo.app.global.exception

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.duckduckgo.app.browser.BuildConfig
import java.text.SimpleDateFormat
import java.util.*

@Entity
data class UncaughtExceptionEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val exceptionSource: UncaughtExceptionSource,
val message: String
)
val message: String,
val timestamp: Long = System.currentTimeMillis(),
val version: String = BuildConfig.VERSION_NAME
) {

fun formattedTimestamp(): String = formatter.format(Date(timestamp))

companion object {
val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import com.duckduckgo.app.global.exception.UncaughtExceptionSource.*
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.*
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.COUNT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_APP_VERSION
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_MESSAGE
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.EXCEPTION_TIMESTAMP
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
import io.reactivex.Completable
import io.reactivex.Completable.*
Expand Down Expand Up @@ -103,11 +105,15 @@ class OfflinePixelSender @Inject constructor(
exceptions.forEach { exception ->
Timber.d("Analysing exception $exception")
val pixelName = determinePixelName(exception)
val params = mapOf(EXCEPTION_MESSAGE to exception.message)
val params = mapOf(
EXCEPTION_MESSAGE to exception.message,
EXCEPTION_APP_VERSION to exception.version,
EXCEPTION_TIMESTAMP to exception.formattedTimestamp()
)

val pixel = pixel.fireCompletable(pixelName, params)
.doOnComplete {
Timber.d("Sent pixel containing exception; deleting exception with id=${exception.id}")
Timber.d("Sent pixel with params: $params containing exception; deleting exception with id=${exception.id}")
runBlocking { uncaughtExceptionRepository.deleteException(exception.id) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ interface Pixel {
const val URL = "url"
const val COUNT = "count"
const val EXCEPTION_MESSAGE = "m"
const val EXCEPTION_APP_VERSION = "v"
const val EXCEPTION_TIMESTAMP = "t"
const val BOOKMARK_CAPABLE = "bc"
const val SHOWED_BOOKMARKS = "sb"
const val DEFAULT_BROWSER_BEHAVIOUR_TRIGGERED = "bt"
Expand Down