Skip to content
Open
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
@@ -1,7 +1,7 @@
package com.virtusize.android.constants

object Constants {
const val COMPILE_SDK = 34
const val COMPILE_SDK = 35
const val MIN_SDK = 21
const val TARGET_SDK = 35

Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ truth = "1.4.4"
virtusize = "2.12.21"
virtusizeAuth = "1.1.1"
browser = "1.8.0"
sentry = "8.34.1"
sentry_plugin = "6.1.0"

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
Expand Down Expand Up @@ -51,6 +53,7 @@ virtusize-auth = { module = "com.virtusize.android:virtusize-auth", version.ref
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
coil = { group = "io.coil-kt", name = "coil", version = "2.6.0" }
coil-gif = { group = "io.coil-kt", name = "coil-gif", version = "2.6.0" }
sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand All @@ -59,3 +62,4 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nextPublish" }
sentry-android = { id = "io.sentry.android.gradle", version.ref = "sentry_plugin" }
3 changes: 3 additions & 0 deletions virtusize/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.sentry.android)
`maven-publish`
signing
}
Expand Down Expand Up @@ -90,6 +91,8 @@ dependencies {
implementation(libs.coil)
implementation(libs.coil.gif)

implementation(libs.sentry.android)

debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(libs.androidx.test.core)
Expand Down
17 changes: 17 additions & 0 deletions virtusize/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:name=".ui.VirtusizeWebViewActivity" />

<!-- Required: set your sentry.io project identifier (DSN) -->
<meta-data
android:name="io.sentry.dsn"
android:value="https://fc419e92ef1a49b9cef57d1c6f207e75@o903.ingest.us.sentry.io/4510877744562176"
/>
<!-- Add data like request headers, user ip address and device name, see https://docs.sentry.io/platforms/android/data-management/data-collected/ for more info -->
<meta-data
android:name="io.sentry.send-default-pii"
android:value="true"
/>
<!-- Enable Sentry structured Logs feature (requires sentry-android >= 8.0.0) -->
<meta-data
android:name="io.sentry.logs.enabled"
android:value="true" />
<meta-data
android:name="io.sentry.metrics.enabled"
android:value="true" />
</application>

</manifest>
105 changes: 96 additions & 9 deletions virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import com.virtusize.android.util.trimI18nText
import com.virtusize.android.util.valueOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
Expand All @@ -47,6 +49,16 @@ internal class VirtusizeImpl(
// Registered message handlers
private val messageHandlers = mutableListOf<VirtusizeMessageHandler>()

// Sentry store ID helper
private val sentryStoreId: String?
get() = VirtusizeApi.currentStoreId?.value?.toString()

// Job to track and cancel previous load operation
private var loadJob: Job? = null

// Tracks the last product ID loaded to detect product changes for session tracking
private var lastLoadedProductExternalId: String? = null

// The Virtusize message handler passes received errors and events to registered message handlers
val messageHandler =
object : VirtusizeMessageHandler {
Expand All @@ -60,6 +72,7 @@ internal class VirtusizeImpl(
// Handle different user events from the web view
when (event) {
is VirtusizeEvent.UserAddedProduct -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
scope.launch {
virtusizeRepository.fetchDataForInPageRecommendation(
shouldUpdateUserProducts = true,
Expand All @@ -72,12 +85,14 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserAuthData -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
event.data?.let { data ->
virtusizeRepository.updateUserAuthData(data)
}
}

is VirtusizeEvent.UserChangedRecommendationType -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
// Switches the view for InPage based on user selected size recommendation type
var recommendationType: SizeRecommendationType? = null
event.data?.optString("recommendationType")?.let {
Expand All @@ -91,6 +106,7 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserLoggedOut, is VirtusizeEvent.UserDeletedData -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
// Clears user related data and updates the session,
// and then re-fetches user products and body profile from the server
scope.launch {
Expand All @@ -102,6 +118,7 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserDeletedProduct -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
event.data?.optInt("userProductId")?.let { userProductId ->
virtusizeRepository.deleteUserProduct(userProductId)
}
Expand All @@ -115,6 +132,7 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserLoggedIn -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
// Updates the user session and fetches updated user products and body profile from the server
scope.launch {
virtusizeRepository.updateUserSession()
Expand All @@ -124,6 +142,10 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserOpenedWidget -> {
VirtusizeSentryTracker.trackWebViewEvent(
eventName = event.name,
storeId = sentryStoreId,
)
virtusizeRepository.setLastProductOnVirtusizeWebView(product.externalId)
scope.launch {
virtusizeRepository.fetchDataForInPageRecommendation(
Expand All @@ -135,6 +157,7 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserSelectedProduct -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
val userProductId = event.data?.optInt("userProductId")
scope.launch {
virtusizeRepository.fetchDataForInPageRecommendation(
Expand All @@ -149,6 +172,7 @@ internal class VirtusizeImpl(
}

is VirtusizeEvent.UserUpdatedBodyMeasurements -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
invalidateCurrentProduct()
// Updates the body recommendation size and switches the view to the body comparison
val sizeRecName = event.data?.optString("sizeRecName")
Expand All @@ -160,12 +184,18 @@ internal class VirtusizeImpl(
}
}

is VirtusizeEvent.UserClosedWidget ->
is VirtusizeEvent.UserClosedWidget -> {
VirtusizeSentryTracker.trackWebViewEvent(
eventName = event.name,
storeId = sentryStoreId,
)
scope.launch {
virtusizeRepository.updateUserSession()
}
}

is VirtusizeEvent.UserClickedLanguageSelector -> {
VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)
event.data?.optString("language")?.let { language ->
val virtusizeLanguage = VirtusizeLanguage.entries.firstOrNull { it.value == language }
if (virtusizeLanguage != null) {
Expand All @@ -174,20 +204,26 @@ internal class VirtusizeImpl(
}
}

is VirtusizeEvent.UserSawProduct -> Unit

is VirtusizeEvent.UserCreatedSilhouette,
is VirtusizeEvent.UserSawProduct,
is VirtusizeEvent.UserSawWidgetButton,
is VirtusizeEvent.UserClickedStart,
is VirtusizeEvent.WidgetReady,
is VirtusizeEvent.Undefined,
-> Unit
-> VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId)

is VirtusizeEvent.Undefined -> Unit
}
}

override fun onError(error: VirtusizeError) {
messageHandlers.forEach { messageHandler ->
messageHandler.onError(error)
}
VirtusizeSentryTracker.trackError(
throwable = Exception(error.message),
storeId = sentryStoreId,
)
}
}

Expand Down Expand Up @@ -322,10 +358,10 @@ internal class VirtusizeImpl(
/**
* @see Virtusize.setApiKey
*/
override fun setApiKey(apiKey: String) {
VirtusizeApi.setApiKey(apiKey)
override fun setApiKey(newApiKey: String) {
VirtusizeApi.setApiKey(newApiKey)
virtusizeViews.forEach { virtusizeView ->
virtusizeView.virtusizeParams.apiKey = apiKey
virtusizeView.virtusizeParams.apiKey = newApiKey
}
}

Expand Down Expand Up @@ -357,9 +393,50 @@ internal class VirtusizeImpl(
* @see Virtusize.load
*/
override fun load(virtusizeProduct: VirtusizeProduct) {
scope.launch {
productCheck(virtusizeProduct)
// Generate a new Sentry session ID when a different product is loaded
if (lastLoadedProductExternalId != virtusizeProduct.externalId) {
lastLoadedProductExternalId = virtusizeProduct.externalId
VirtusizeSentryTracker.generateSessionId()
}
loadJob?.cancel()
loadJob =
scope.launch {
if (!isActive) {
VirtusizeSentryTracker.trackLoadCancelled(
step = "start",
externalProductId = virtusizeProduct.externalId,
storeId = sentryStoreId,
)
return@launch
}
val success = productCheck(virtusizeProduct)
if (!isActive) {
VirtusizeSentryTracker.trackLoadCancelled(
step = "product-check",
externalProductId = virtusizeProduct.externalId,
storeId = sentryStoreId,
)
return@launch
}
// productCheckData is set only when the API call itself succeeded (valid or invalid product)
val apiSucceeded = virtusizeProduct.productCheckData != null
VirtusizeSentryTracker.trackProductCheck(
externalProductId = virtusizeProduct.externalId,
isValid = success,
storeId = if (apiSucceeded) sentryStoreId else null,
)
if (apiSucceeded) {
VirtusizeSentryTracker.trackUserSawProduct(
externalProductId = virtusizeProduct.externalId,
storeId = sentryStoreId,
)
} else {
VirtusizeSentryTracker.trackError(
throwable = Exception("Product check failed"),
storeId = null,
)
}
}
}

/**
Expand Down Expand Up @@ -402,9 +479,14 @@ internal class VirtusizeImpl(
onError: ((VirtusizeError) -> Unit)?,
) {
scope.launch {
VirtusizeSentryTracker.trackSendOrder(order, sentryStoreId)
virtusizeRepository.sendOrder(params, order, { _ ->
onSuccess?.invoke()
}, { error ->
VirtusizeSentryTracker.trackError(
throwable = Exception(error.message),
storeId = sentryStoreId,
)
onError?.invoke(error)
})
}
Expand All @@ -419,9 +501,14 @@ internal class VirtusizeImpl(
onError: ErrorResponseHandler?,
) {
scope.launch {
VirtusizeSentryTracker.trackSendOrder(order, sentryStoreId)
virtusizeRepository.sendOrder(params, order, { data ->
onSuccess?.onSuccess(data)
}, { error ->
VirtusizeSentryTracker.trackError(
throwable = Exception(error.message),
storeId = sentryStoreId,
)
onError?.onError(error)
})
}
Expand Down
Loading