Skip to content

Commit

Permalink
fix: modal analytics fixes (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
merlinpaypal committed May 14, 2024
1 parent ff2ac5a commit c95246f
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 77 deletions.
20 changes: 15 additions & 5 deletions library/src/main/java/com/paypal/messages/ModalFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import com.paypal.messages.analytics.AnalyticsComponent
import com.paypal.messages.analytics.AnalyticsEvent
Expand All @@ -53,6 +54,7 @@ internal class ModalFragment(
) : BottomSheetDialogFragment() {
private val TAG = "PayPalMessageModal"
private val offsetTop = 50.dp
private val gson = GsonBuilder().setPrettyPrinting().create()

private var modalUrl: String? = null

Expand Down Expand Up @@ -118,10 +120,7 @@ internal class ModalFragment(
val colorInt = Color.parseColor(this.closeButtonData?.color)
closeButton.background.colorFilter = PorterDuffColorFilter(colorInt, PorterDuff.Mode.SRC_ATOP)

closeButton?.setOnClickListener {
logEvent(AnalyticsEvent(eventType = EventType.MODAL_CLOSED))
dialog?.hide()
}
closeButton?.setOnClickListener { dialog?.hide() }

// If we already have a WebView, don't reset it
LogCat.debug(TAG, "Configuring WebView Settings and Handlers")
Expand Down Expand Up @@ -371,7 +370,7 @@ internal class ModalFragment(
val nameAndArgs = JsonParser.parseString(params).asJsonObject
val name = nameAndArgs.get("name").asString
val args = nameAndArgs.get("args").asJsonArray[0].asJsonObject
LogCat.debug(TAG, "CallbackHandler:\n name = $name\n args = $args")
LogCat.debug(TAG, "CallbackHandler:\n name = $name\n args = ${gson.toJson(args)}")

// If __shared__ does not exist, use an empty object
val sharedJson = args.get("__shared__") ?: JsonParser.parseString("{}")
Expand Down Expand Up @@ -408,6 +407,17 @@ internal class ModalFragment(
)
}

"onReady" -> {
logEvent(
AnalyticsEvent(
eventType = EventType.MODAL_RENDERED,
renderDuration = args.get("render_duration")?.asString ?: "",
requestDuration = args.get("request_duration")?.asString ?: "",
),
shared,
)
}

else -> {
// do something else or throw an exception
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.paypal.messages.analytics

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.annotations.SerializedName
import com.paypal.messages.config.PayPalMessageOfferType
import com.paypal.messages.config.PayPalMessagePageType
import com.paypal.messages.config.message.PayPalMessageStyle
import com.paypal.messages.config.message.style.PayPalMessageAlignment
import com.paypal.messages.config.message.style.PayPalMessageColor
import com.paypal.messages.config.message.style.PayPalMessageLogoType
import com.paypal.messages.extensions.getJsonObject
import com.paypal.messages.utils.KoverExcludeGenerated

/**
* [AnalyticsComponent] holds data used for logging analytics information
Expand Down Expand Up @@ -73,4 +78,23 @@ data class AnalyticsComponent(
// Dynamic Properties, not serialized by default
@Suppress("PropertyName")
val __shared__: MutableMap<String, Any>? = mutableMapOf(),
)
) {
// TODO: Move all Gson to its own wrapping class
@KoverExcludeGenerated
fun getData(): JsonObject {
val gson: Gson = GsonBuilder().setPrettyPrinting().create()
val componentJson = gson.getJsonObject(this)

val sharedJson = componentJson.get("__shared__")
if (sharedJson != null) {
val sharedJsonCopy = sharedJson.deepCopy().asJsonObject
componentJson.remove("__shared__")

for ((key, value) in sharedJsonCopy.entrySet()) {
componentJson.add(key, value)
}
}

return componentJson
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.paypal.messages.analytics

import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.paypal.messages.extensions.getJsonElement
import com.paypal.messages.extensions.getJsonObject
import com.paypal.messages.io.Api
import com.paypal.messages.io.LocalStorage
import com.paypal.messages.utils.LogCat
Expand All @@ -12,6 +16,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class AnalyticsLogger private constructor() {
private val gson = GsonBuilder().setPrettyPrinting().create()
companion object {
private const val TAG: String = "AnalyticsLogger"

Expand Down Expand Up @@ -99,6 +104,12 @@ class AnalyticsLogger private constructor() {

private var job: Job? = null
private fun sendEvent(context: Context, finalPayload: AnalyticsPayload) {
val payloadJson = gson.getJsonObject(finalPayload)

val jsonArray = JsonArray()
finalPayload.components.forEach { jsonArray.add(it.getData()) }
payloadJson.add("components", jsonArray)

job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
delay(5000) // Wait 5 seconds before sending our payload
Expand All @@ -111,8 +122,8 @@ class AnalyticsLogger private constructor() {
}
LogCat.debug(TAG, "merchantHash: ${hash}\npayloadSummary:\n$payloadSummary")

finalPayload.merchantProfileHash = hash
Api.callLoggerEndpoint(finalPayload)
payloadJson.add("merchant_profile_hash", gson.getJsonElement("$hash"))
Api.callLoggerEndpoint(payloadJson)
resetBasePayload()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.paypal.messages.analytics

import com.google.gson.JsonObject
import com.google.gson.annotations.SerializedName
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID

data class CloudEvent(
Expand All @@ -17,10 +19,10 @@ data class CloudEvent(
@SerializedName("dataschema")
val dataSchema: String = "ppaas:events.credit.FinancingPresentmentAsyncAPISpecification/v1/schema/json/credit_upstream_presentment_event.json",
var time: String = "",
val data: AnalyticsPayload,
val data: JsonObject,
) {
init {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
val currentDate = Date()

val formattedDate = dateFormat.format(currentDate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ enum class EventType {
@SerializedName("modal_viewed")
MODAL_VIEWED,

@SerializedName("modal_closed")
MODAL_CLOSED,

@SerializedName("modal_error")
MODAL_ERROR,
;
Expand Down
9 changes: 9 additions & 0 deletions library/src/main/java/com/paypal/messages/extensions/Gson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.paypal.messages.extensions

import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlin.reflect.KClass

fun KClass<Gson>.jsonElementToMutableMap(jsonElement: JsonElement): MutableMap<String, Any> {
Expand Down Expand Up @@ -32,3 +33,11 @@ fun KClass<Gson>.jsonValueToAny(jsonElement: JsonElement): Any {
else -> throw IllegalArgumentException("Unsupported JSON element type: ${jsonElement::class.java.simpleName}")
}
}

fun Gson.getJsonObject(value: Any): JsonObject {
return this.fromJson(this.toJson(value), JsonObject::class.java)
}

fun Gson.getJsonElement(value: Any): JsonElement {
return this.fromJson(this.toJson(value), JsonElement::class.java)
}
48 changes: 4 additions & 44 deletions library/src/main/java/com/paypal/messages/io/Api.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.paypal.messages.io

import android.annotation.SuppressLint
import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.paypal.messages.BuildConfig
import com.paypal.messages.analytics.AnalyticsPayload
import com.paypal.messages.analytics.CloudEvent
import com.paypal.messages.utils.LogCat
import com.paypal.messages.utils.PayPalErrors
Expand All @@ -14,59 +13,20 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okio.IOException
import org.json.JSONObject
import java.security.cert.X509Certificate
import java.util.UUID
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import com.paypal.messages.config.PayPalEnvironment as Env
import com.paypal.messages.config.PayPalMessageOfferType as OfferType
import com.paypal.messages.config.message.PayPalMessageConfig as MessageConfig

object Api {
private const val TAG = "Api"

private val trustAllCerts = arrayOf<TrustManager>(
@SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}

@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}

override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
},
)

private val sslContext: SSLContext = SSLContext.getInstance("SSL")
.apply {
this.init(null, trustAllCerts, java.security.SecureRandom())
}

// Create an ssl socket factory with our all-trusting manager
private val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory

// connect to server
@Suppress("unused")
private val insecureClient = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.build()

private val client = OkHttpClient()
// if connecting to a test environment, use ApiClient.insecureClient
private val client = ApiClient.secureClient
private val gson = GsonBuilder().setPrettyPrinting().create()
var env = Env.SANDBOX
var devTouchpoint: Boolean = false
Expand Down Expand Up @@ -323,7 +283,7 @@ object Api {
return jsonObject.toString()
}

fun callLoggerEndpoint(payload: AnalyticsPayload) {
fun callLoggerEndpoint(payload: JsonObject) {
val json = gson.toJson(CloudEvent(data = payload))
val request = createLoggerRequest(preventEmptyValues(json))
val response = client.newCall(request).execute()
Expand Down
52 changes: 52 additions & 0 deletions library/src/main/java/com/paypal/messages/io/ApiClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.paypal.messages.io

import android.annotation.SuppressLint
import com.paypal.messages.utils.KoverExcludeGenerated
import okhttp3.OkHttpClient
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

object ApiClient {
// trustAllCerts is only used for connecting to test environments
private val trustAllCerts = arrayOf<TrustManager>(
@SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@KoverExcludeGenerated
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}

@SuppressLint("TrustAllX509TrustManager")
@KoverExcludeGenerated
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}

override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
},
)

// sslContext is only used for connecting to test environments
private val sslContext: SSLContext = SSLContext.getInstance("SSL")
.apply {
this.init(null, trustAllCerts, java.security.SecureRandom())
}

// sslSocketFactor is only used for connecting to test environments
private val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory

// insecureClient is only used for connecting to test environments
@Suppress("unused")
internal val insecureClient = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.build()

internal val secureClient = OkHttpClient()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.paypal.messages.analytics

import com.google.gson.Gson
import com.paypal.messages.extensions.getJsonObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
Expand All @@ -24,17 +25,20 @@ class CloudEventTest {
@Suppress("ktlint:standard:max-line-length")
private val dataSchema = "ppaas:events.credit.FinancingPresentmentAsyncAPISpecification/v1/schema/json/credit_upstream_presentment_event.json"

private val data = AnalyticsPayload(
clientId = clientId,
merchantId = merchantId,
partnerAttributionId = partnerAttributionId,
merchantProfileHash = merchantProfileHash,
instanceId = instanceId,
integrationName = integrationName,
integrationType = integrationType,
integrationVersion = integrationVersion,
libraryVersion = libraryVersion,
components = components,
private val gson = Gson()
private val data = gson.getJsonObject(
AnalyticsPayload(
clientId = clientId,
merchantId = merchantId,
partnerAttributionId = partnerAttributionId,
merchantProfileHash = merchantProfileHash,
instanceId = instanceId,
integrationName = integrationName,
integrationType = integrationType,
integrationVersion = integrationVersion,
libraryVersion = libraryVersion,
components = components,
),
)

private val cloudWrappedEvent = CloudEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ class EventTypeTest {
assertEquals(EventType.MODAL_VIEWED.toString(), "modal_viewed")
}

@Test
fun testModalClose() {
assertEquals(EventType.MODAL_CLOSED.toString(), "modal_closed")
}

@Test
fun testModalError() {
assertEquals(EventType.MODAL_ERROR.toString(), "modal_error")
Expand Down

0 comments on commit c95246f

Please sign in to comment.