Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix analytic event timestamps #1160

Merged
merged 2 commits into from
Sep 19, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.braintreepayments.api.sharedutils.Time
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
Expand All @@ -20,7 +21,8 @@ internal class AnalyticsClient(
private val analyticsDatabase: AnalyticsDatabase = AnalyticsDatabase.getInstance(context.applicationContext),
private val workManager: WorkManager = WorkManager.getInstance(context.applicationContext),
private val deviceInspector: DeviceInspector = DeviceInspector(),
private val analyticsParamRepository: AnalyticsParamRepository = AnalyticsParamRepository.instance
private val analyticsParamRepository: AnalyticsParamRepository = AnalyticsParamRepository.instance,
private val time: Time = Time()
) {
private val applicationContext = context.applicationContext

Expand Down Expand Up @@ -121,6 +123,7 @@ internal class AnalyticsClient(
)
val analyticsRequest =
createFPTIPayload(authorization, eventBlobs, metadata)

httpClient.post(
FPTI_ANALYTICS_URL,
analyticsRequest.toString(),
Expand All @@ -137,27 +140,11 @@ internal class AnalyticsClient(
}
}

fun reportCrash(
context: Context?,
configuration: Configuration?,
integration: IntegrationType?,
authorization: Authorization?
) {
reportCrash(
context,
configuration,
integration,
System.currentTimeMillis(),
authorization
)
}

@VisibleForTesting
fun reportCrash(
context: Context?,
configuration: Configuration?,
integration: IntegrationType?,
timestamp: Long,
authorization: Authorization?
) {
if (authorization == null) {
Expand All @@ -169,7 +156,10 @@ internal class AnalyticsClient(
sessionId = analyticsParamRepository.sessionId,
integration = integration
)
val event = AnalyticsEvent(name = "crash", timestamp = timestamp)
val event = AnalyticsEvent(
name = "crash",
timestamp = time.currentTime
)
val eventJSON = mapAnalyticsEventToFPTIEventJSON(event)
val eventBlobs = listOf(
AnalyticsEventBlob(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package com.braintreepayments.api.core

internal data class AnalyticsEvent(
val name: String,
val timestamp: Long,
val payPalContextId: String? = null,
val linkType: String? = null,
val isVaultRequest: Boolean = false,
val startTime: Long? = null,
val endTime: Long? = null,
val endpoint: String? = null,
val timestamp: Long = System.currentTimeMillis(),
val endpoint: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.annotation.VisibleForTesting
import com.braintreepayments.api.sharedutils.HttpResponseCallback
import com.braintreepayments.api.sharedutils.HttpResponseTiming
import com.braintreepayments.api.sharedutils.ManifestValidator
import com.braintreepayments.api.sharedutils.Time
import org.json.JSONException
import org.json.JSONObject

Expand Down Expand Up @@ -38,6 +39,7 @@ class BraintreeClient @VisibleForTesting internal constructor(
private val graphQLClient: BraintreeGraphQLClient,
private val configurationLoader: ConfigurationLoader,
private val manifestValidator: ManifestValidator,
private val time: Time,
private val returnUrlScheme: String,
private val braintreeDeepLinkReturnUrlScheme: String,
/**
Expand All @@ -50,7 +52,10 @@ class BraintreeClient @VisibleForTesting internal constructor(
private var launchesBrowserSwitchAsNewTask: Boolean = false

// NOTE: this constructor is used to make dependency injection easy
internal constructor(params: BraintreeClientParams) : this(
internal constructor(
params: BraintreeClientParams,
time: Time = Time()
) : this(
applicationContext = params.applicationContext,
integrationType = params.integrationType,
authorization = params.authorization,
Expand All @@ -59,6 +64,7 @@ class BraintreeClient @VisibleForTesting internal constructor(
graphQLClient = params.graphQLClient,
configurationLoader = params.configurationLoader,
manifestValidator = params.manifestValidator,
time = time,
returnUrlScheme = params.returnUrlScheme,
braintreeDeepLinkReturnUrlScheme = params.braintreeReturnUrlScheme,
appLinkReturnUri = params.appLinkReturnUri
Expand Down Expand Up @@ -132,15 +138,17 @@ class BraintreeClient @VisibleForTesting internal constructor(
eventName: String,
params: AnalyticsEventParams = AnalyticsEventParams()
) {
val timestamp = time.currentTime
getConfiguration { configuration, _ ->
val event = AnalyticsEvent(
eventName,
params.payPalContextId,
params.linkType,
params.isVaultRequest,
params.startTime,
params.endTime,
params.endpoint
name = eventName,
timestamp = timestamp,
payPalContextId = params.payPalContextId,
linkType = params.linkType,
isVaultRequest = params.isVaultRequest,
startTime = params.startTime,
endTime = params.endTime,
endpoint = params.endpoint,
)
sendAnalyticsEvent(event, configuration, authorization)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.braintreepayments.api.core.AnalyticsClient.Companion.WORK_INPUT_KEY_S
import com.braintreepayments.api.core.AnalyticsClient.Companion.WORK_NAME_ANALYTICS_UPLOAD
import com.braintreepayments.api.core.Authorization.Companion.fromString
import com.braintreepayments.api.core.Configuration.Companion.fromJson
import com.braintreepayments.api.sharedutils.Time
import com.braintreepayments.api.testutils.Fixtures
import io.mockk.*
import org.json.JSONException
Expand All @@ -30,6 +31,7 @@ class AnalyticsClientUnitTest {
private lateinit var httpClient: BraintreeHttpClient
private lateinit var deviceInspector: DeviceInspector
private lateinit var analyticsParamRepository: AnalyticsParamRepository
private lateinit var time: Time
private lateinit var eventName: String
private lateinit var sessionId: String
private lateinit var payPalContextId: String
Expand All @@ -39,6 +41,8 @@ class AnalyticsClientUnitTest {
private lateinit var analyticsDatabase: AnalyticsDatabase
private lateinit var analyticsEventBlobDao: AnalyticsEventBlobDao

private lateinit var sut: AnalyticsClient

private var timestamp: Long = 0

@Before
Expand All @@ -59,9 +63,22 @@ class AnalyticsClientUnitTest {
analyticsDatabase = mockk(relaxed = true)
analyticsEventBlobDao = mockk(relaxed = true)
workManager = mockk(relaxed = true)
time = mockk(relaxed = true)

every { analyticsDatabase.analyticsEventBlobDao() } returns analyticsEventBlobDao
every { analyticsParamRepository.sessionId } returns sessionId

every { time.currentTime } returns 123

sut = AnalyticsClient(
context = context,
httpClient = httpClient,
analyticsDatabase = analyticsDatabase,
workManager = workManager,
deviceInspector = deviceInspector,
analyticsParamRepository = analyticsParamRepository,
time = time
)
}

@Test
Expand All @@ -77,13 +94,7 @@ class AnalyticsClientUnitTest {
} returns mockk()

val event = AnalyticsEvent(eventName, timestamp = 123)
val sut = AnalyticsClient(
context = context,
httpClient = httpClient,
analyticsDatabase = analyticsDatabase,
workManager = workManager,
deviceInspector = deviceInspector
)

sut.sendEvent(configuration, event, integration, authorization)

val workSpec = workRequestSlot.captured.workSpec
Expand Down Expand Up @@ -120,8 +131,7 @@ class AnalyticsClientUnitTest {
isVaultRequest = true,
timestamp = 456
)
val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)

sut.sendEvent(configuration, event, integration, authorization)

val workSpec = workRequestSlot.captured.workSpec
Expand Down Expand Up @@ -156,17 +166,13 @@ class AnalyticsClientUnitTest {
.putString(WORK_INPUT_KEY_ANALYTICS_JSON, JSONObject().toString())
.putString(WORK_INPUT_KEY_SESSION_ID, JSONObject().toString())
.build()
val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsWrite(inputData)
assertTrue(result is ListenableWorker.Result.Success)
}

@Test
fun writeAnalytics_whenAnalyticsJSONIsMissing_returnsSuccess() {
val inputData = Data.Builder().build()
val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsWrite(inputData)
assertTrue(result is ListenableWorker.Result.Failure)
}
Expand All @@ -181,8 +187,7 @@ class AnalyticsClientUnitTest {
.putString(WORK_INPUT_KEY_ANALYTICS_JSON, json)
.putString(WORK_INPUT_KEY_SESSION_ID, sessionId)
.build()
val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)

sut.performAnalyticsWrite(inputData)

val blob = analyticsEventBlobSlot.captured
Expand Down Expand Up @@ -238,8 +243,6 @@ class AnalyticsClientUnitTest {
)
}

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
sut.performAnalyticsUpload(inputData)

// language=JSON
Expand Down Expand Up @@ -285,8 +288,6 @@ class AnalyticsClientUnitTest {
.putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration.stringValue)
.build()

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsUpload(inputData)
assertTrue(result is ListenableWorker.Result.Failure)

Expand All @@ -303,8 +304,6 @@ class AnalyticsClientUnitTest {
.putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration.stringValue)
.build()

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsUpload(inputData)
assertTrue(result is ListenableWorker.Result.Failure)

Expand Down Expand Up @@ -340,8 +339,6 @@ class AnalyticsClientUnitTest {
val analyticsJSONSlot = slot<String>()
every { httpClient.post(any(), capture(analyticsJSONSlot), any(), any()) }

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
sut.performAnalyticsUpload(inputData)

val analyticsJson = JSONObject(analyticsJSONSlot.captured)
Expand All @@ -360,8 +357,6 @@ class AnalyticsClientUnitTest {
.putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration.stringValue)
.build()

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsUpload(inputData)
assertTrue(result is ListenableWorker.Result.Failure)

Expand All @@ -378,8 +373,6 @@ class AnalyticsClientUnitTest {
.putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId)
.build()

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsUpload(inputData)
assertTrue(result is ListenableWorker.Result.Failure)

Expand Down Expand Up @@ -410,8 +403,6 @@ class AnalyticsClientUnitTest {
)
every { analyticsEventBlobDao.getBlobsBySessionId(sessionId) } returns blobs

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
sut.performAnalyticsUpload(inputData)

verify { analyticsEventBlobDao.deleteEventBlobs(blobs) }
Expand Down Expand Up @@ -442,8 +433,6 @@ class AnalyticsClientUnitTest {
val httpError = Exception("error")
every { httpClient.post(any(), any(), any(), any()) } throws httpError

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val result = sut.performAnalyticsUpload(inputData)
assertTrue(result is ListenableWorker.Result.Failure)
}
Expand All @@ -469,15 +458,7 @@ class AnalyticsClientUnitTest {
)
} returns Unit

val sut = AnalyticsClient(
context = context,
httpClient = httpClient,
analyticsDatabase = analyticsDatabase,
workManager = workManager,
deviceInspector = deviceInspector,
analyticsParamRepository = analyticsParamRepository
)
sut.reportCrash(context, configuration, integration, 123, authorization)
sut.reportCrash(context, configuration, integration, authorization)

// language=JSON
val expectedJSON = """
Expand Down Expand Up @@ -527,29 +508,18 @@ class AnalyticsClientUnitTest {
deviceInspector.getDeviceMetadata(context, configuration, sessionId, integration)
} returns metadata

val sut =
AnalyticsClient(context, httpClient, analyticsDatabase, workManager, deviceInspector)
val event = AnalyticsEvent(eventName)
val event = AnalyticsEvent(eventName, timestamp)
sut.sendEvent(configuration, event, integration, authorization)

sut.reportCrash(context, configuration, integration, 123, null)
sut.reportCrash(context, configuration, integration, null)

// or confirmVerified(httpClient)
verify { httpClient wasNot Called }
}

@Test
fun `sendEvent enqueues work to upload analytic events with sessionId in the name`() {
val sut = AnalyticsClient(
context = context,
httpClient = httpClient,
analyticsDatabase = analyticsDatabase,
workManager = workManager,
deviceInspector = deviceInspector,
analyticsParamRepository = analyticsParamRepository
)

sut.sendEvent(configuration, AnalyticsEvent("event-name"), integration, authorization)
sut.sendEvent(configuration, AnalyticsEvent("event-name", timestamp), integration, authorization)

verify {
workManager.enqueueUniqueWork(
Expand Down
Loading
Loading