From 312d86c9f665222cc232148aeae3b51db7d6ffda Mon Sep 17 00:00:00 2001 From: Michelle Dayangco Date: Fri, 13 Feb 2026 18:23:35 +0800 Subject: [PATCH 1/6] Integrate Sentry --- gradle/libs.versions.toml | 4 ++++ virtusize/build.gradle.kts | 3 +++ virtusize/src/main/AndroidManifest.xml | 3 +++ 3 files changed, 10 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5a879cd..d6e193f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,8 @@ truth = "1.4.4" virtusize = "2.12.21" virtusizeAuth = "1.1.1" browser = "1.8.0" +sentry = "7.10.0" +sentry_plugin = "4.3.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -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" } @@ -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" } diff --git a/virtusize/build.gradle.kts b/virtusize/build.gradle.kts index 5ac323d2..24992a22 100644 --- a/virtusize/build.gradle.kts +++ b/virtusize/build.gradle.kts @@ -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 } @@ -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) diff --git a/virtusize/src/main/AndroidManifest.xml b/virtusize/src/main/AndroidManifest.xml index 8b7696cd..fcc87f14 100644 --- a/virtusize/src/main/AndroidManifest.xml +++ b/virtusize/src/main/AndroidManifest.xml @@ -9,6 +9,9 @@ android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:name=".ui.VirtusizeWebViewActivity" /> + From 1967a88beb5383eeca5b17486397f13bf824cd0a Mon Sep 17 00:00:00 2001 From: OleS Date: Fri, 27 Feb 2026 12:32:11 +0200 Subject: [PATCH 2/6] Align with iOS version + bug fix --- gradle/libs.versions.toml | 4 +- virtusize/src/main/AndroidManifest.xml | 16 ++- .../com/virtusize/android/VirtusizeImpl.kt | 96 ++++++++++++-- .../android/VirtusizeSentryTracker.kt | 125 ++++++++++++++++++ 4 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6e193f0..19074173 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,8 +23,8 @@ truth = "1.4.4" virtusize = "2.12.21" virtusizeAuth = "1.1.1" browser = "1.8.0" -sentry = "7.10.0" -sentry_plugin = "4.3.0" +sentry = "8.34.1" +sentry_plugin = "6.1.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } diff --git a/virtusize/src/main/AndroidManifest.xml b/virtusize/src/main/AndroidManifest.xml index fcc87f14..bee9ac97 100644 --- a/virtusize/src/main/AndroidManifest.xml +++ b/virtusize/src/main/AndroidManifest.xml @@ -9,9 +9,23 @@ android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:name=".ui.VirtusizeWebViewActivity" /> + + android:value="https://fc419e92ef1a49b9cef57d1c6f207e75@o903.ingest.us.sentry.io/4510877744562176" + /> + + + + + diff --git a/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt b/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt index d32802c4..7efec7b2 100644 --- a/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt +++ b/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt @@ -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 /** @@ -47,6 +49,16 @@ internal class VirtusizeImpl( // Registered message handlers private val messageHandlers = mutableListOf() + // 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 { @@ -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, @@ -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 { @@ -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 { @@ -102,6 +118,7 @@ internal class VirtusizeImpl( } is VirtusizeEvent.UserDeletedProduct -> { + VirtusizeSentryTracker.trackWebViewEvent(event.name, sentryStoreId) event.data?.optInt("userProductId")?.let { userProductId -> virtusizeRepository.deleteUserProduct(userProductId) } @@ -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() @@ -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( @@ -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( @@ -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") @@ -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) { @@ -174,13 +204,15 @@ 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 } } @@ -188,6 +220,10 @@ internal class VirtusizeImpl( messageHandlers.forEach { messageHandler -> messageHandler.onError(error) } + VirtusizeSentryTracker.trackError( + throwable = Exception(error.message), + storeId = sentryStoreId, + ) } } @@ -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 } } @@ -357,8 +393,40 @@ 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, + ) + } } } @@ -402,9 +470,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) }) } @@ -419,9 +492,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) }) } diff --git a/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt new file mode 100644 index 00000000..c5ea2f28 --- /dev/null +++ b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt @@ -0,0 +1,125 @@ +package com.virtusize.android + +import com.virtusize.android.data.local.VirtusizeOrder +import io.sentry.Sentry +import io.sentry.SentryLevel +import io.sentry.metrics.SentryMetricsParameters +import java.util.UUID + +/** + * Utility for Sentry metrics and structured logs tracking in the Virtusize SDK. + * + * Requires Sentry Android SDK >= 8.0.0. + * Configured via AndroidManifest.xml meta-data (DSN, logs, traces sample rate, environment). + */ +internal object VirtusizeSentryTracker { + + // MARK: - Session Management + + /** The current active session ID, set by Virtusize.load. */ + var currentSessionId: String = "" + + /** + * Generates a new UUID session ID, stores it as the current session, and returns it. + * Also configures the Sentry scope so all subsequent logs are tagged with the new session ID. + */ + fun generateSessionId(): String { + currentSessionId = UUID.randomUUID().toString() + Sentry.configureScope { scope -> + scope.setTag("session_id", currentSessionId) + } + return currentSessionId + } + + // MARK: - Metrics (Counters) + + fun increment(key: String, tags: Map = emptyMap()) { + Sentry.metrics().count(key, 1.0) + } + + // MARK: - Logs + + fun logInfo(message: String, attributes: Map = emptyMap()) { + Sentry.logger().info(message, attributes) + } + + fun logWarning(message: String, attributes: Map = emptyMap()) { + Sentry.logger().warn(message, attributes) + } + + fun logError(message: String, attributes: Map = emptyMap()) { + Sentry.logger().error(message, attributes) + } + + // MARK: - WebView Events + + fun trackWebViewEvent( + eventName: String, + storeId: String? = null, + ) { + val tags = mutableMapOf("event_name" to eventName) + storeId?.let { tags["store_id"] = it } + logInfo("webview-$eventName", tags) + } + + fun trackUserSawProduct( + storeId: String? = null, + externalProductId: String? = null, + ) { + val tags = buildTags(storeId = storeId) + externalProductId?.let { tags["external_product_id"] = it } + increment("user.saw.product", tags) + logInfo("user-saw-product", tags) + } + + // MARK: - Product Check + + fun trackProductCheck(externalProductId: String, isValid: Boolean, storeId: String? = null) { + val tags = buildTags(storeId = storeId) + + mapOf("external_product_id" to externalProductId, "is_valid" to isValid.toString()) + logInfo("product-check", tags) + } + + fun trackLoadCancelled(step: String, externalProductId: String, storeId: String? = null) { + val tags = buildTags(storeId = storeId) + + mapOf("external_product_id" to externalProductId, "step" to step) + logWarning("load-cancelled", tags) + } + + // MARK: - Order + + fun trackSendOrder(order: VirtusizeOrder, storeId: String? = null) { + val externalProductIds = order.items.mapNotNull { it.paramsToMap()["externalProductId"] as? String } + if (externalProductIds.isEmpty()) { + val tags = buildTags(storeId = storeId) + increment("order.sent", tags) + } else { + for (externalProductId in externalProductIds) { + val tags = buildTags(storeId = storeId) + tags["external_product_id"] = externalProductId + increment("order.sent", tags) + } + } + logInfo("order-sent", buildTags(storeId = storeId)) + } + + // MARK: - Error + + fun trackError( + throwable: Throwable, + storeId: String? = null, + ) { + val tags = buildTags(storeId = storeId) + + mapOf("error_type" to throwable::class.java.simpleName) + increment("error", tags) + logError(throwable.localizedMessage ?: throwable.message ?: throwable::class.java.simpleName, tags) + } + + // MARK: - Private + + private fun buildTags(storeId: String? = null): MutableMap { + val tags = mutableMapOf() + storeId?.let { tags["store_id"] = it } + return tags + } +} From 67d459474ef2473d29f09dbaf251f48158d4b0d4 Mon Sep 17 00:00:00 2001 From: OleS Date: Tue, 10 Mar 2026 12:46:27 +0200 Subject: [PATCH 3/6] Log SDK version --- .../android/VirtusizeSentryTracker.kt | 20 +++++++++++++++++++ .../android/flutter/VirtusizeFlutter.kt | 2 ++ 2 files changed, 22 insertions(+) diff --git a/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt index c5ea2f28..1c20fbaf 100644 --- a/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt +++ b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt @@ -1,5 +1,6 @@ package com.virtusize.android +import com.virtusize.android.BuildConfig import com.virtusize.android.data.local.VirtusizeOrder import io.sentry.Sentry import io.sentry.SentryLevel @@ -19,6 +20,13 @@ internal object VirtusizeSentryTracker { /** The current active session ID, set by Virtusize.load. */ var currentSessionId: String = "" + init { + Sentry.configureScope { scope -> + scope.setTag("sdk_version", BuildConfig.VERSION_NANE) + scope.setTag("sdk_platform", "android") + } + } + /** * Generates a new UUID session ID, stores it as the current session, and returns it. * Also configures the Sentry scope so all subsequent logs are tagged with the new session ID. @@ -31,6 +39,18 @@ internal object VirtusizeSentryTracker { return currentSessionId } + // MARK: - Platform Override + + /** + * Overrides the `sdk_platform` Sentry scope tag. Call this when the SDK is running inside a + * wrapper (e.g. Flutter) so that the tag reflects the actual integration platform. + */ + fun setSDKPlatform(platform: String) { + Sentry.configureScope { scope -> + scope.setTag("sdk_platform", platform) + } + } + // MARK: - Metrics (Counters) fun increment(key: String, tags: Map = emptyMap()) { diff --git a/virtusize/src/main/java/com/virtusize/android/flutter/VirtusizeFlutter.kt b/virtusize/src/main/java/com/virtusize/android/flutter/VirtusizeFlutter.kt index a1a8ca1d..b6384ff1 100644 --- a/virtusize/src/main/java/com/virtusize/android/flutter/VirtusizeFlutter.kt +++ b/virtusize/src/main/java/com/virtusize/android/flutter/VirtusizeFlutter.kt @@ -5,6 +5,7 @@ import android.content.Context import com.virtusize.android.ErrorResponseHandler import com.virtusize.android.SuccessResponseHandler import com.virtusize.android.VirtusizeRepository +import com.virtusize.android.VirtusizeSentryTracker import com.virtusize.android.data.local.VirtusizeError import com.virtusize.android.data.local.VirtusizeLanguage import com.virtusize.android.data.local.VirtusizeMessageHandler @@ -35,6 +36,7 @@ interface VirtusizeFlutter { ) = if (!Companion::instance.isInitialized) { synchronized(this) { if (!Companion::instance.isInitialized) { + VirtusizeSentryTracker.setSDKPlatform("flutter-android") VirtusizeFlutterImpl( context = context, params = params, From a9c4300e703086b0e31366af643d878d3a38f5b4 Mon Sep 17 00:00:00 2001 From: OleS Date: Tue, 10 Mar 2026 14:26:41 +0200 Subject: [PATCH 4/6] Compile SDK version --- .../src/main/java/com/virtusize/android/constants/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/com/virtusize/android/constants/Constants.kt b/buildSrc/src/main/java/com/virtusize/android/constants/Constants.kt index 38880a61..d44f4723 100644 --- a/buildSrc/src/main/java/com/virtusize/android/constants/Constants.kt +++ b/buildSrc/src/main/java/com/virtusize/android/constants/Constants.kt @@ -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 From d766b123228000a482bc336c1fb8c56d4683b720 Mon Sep 17 00:00:00 2001 From: OleS Date: Tue, 10 Mar 2026 15:47:51 +0200 Subject: [PATCH 5/6] klint fix --- .../com/virtusize/android/VirtusizeImpl.kt | 61 +++++++++++-------- .../android/VirtusizeSentryTracker.kt | 56 +++++++++++------ 2 files changed, 74 insertions(+), 43 deletions(-) diff --git a/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt b/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt index 7efec7b2..8d71e017 100644 --- a/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt +++ b/virtusize/src/main/java/com/virtusize/android/VirtusizeImpl.kt @@ -399,35 +399,44 @@ internal class VirtusizeImpl( 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( + 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, - storeId = sentryStoreId, - ) - } else { - VirtusizeSentryTracker.trackError( - throwable = Exception("Product check failed"), - storeId = null, + 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, + ) + } } - } } /** diff --git a/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt index 1c20fbaf..f1a1ff4c 100644 --- a/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt +++ b/virtusize/src/main/java/com/virtusize/android/VirtusizeSentryTracker.kt @@ -1,10 +1,7 @@ package com.virtusize.android -import com.virtusize.android.BuildConfig import com.virtusize.android.data.local.VirtusizeOrder import io.sentry.Sentry -import io.sentry.SentryLevel -import io.sentry.metrics.SentryMetricsParameters import java.util.UUID /** @@ -14,7 +11,6 @@ import java.util.UUID * Configured via AndroidManifest.xml meta-data (DSN, logs, traces sample rate, environment). */ internal object VirtusizeSentryTracker { - // MARK: - Session Management /** The current active session ID, set by Virtusize.load. */ @@ -53,21 +49,33 @@ internal object VirtusizeSentryTracker { // MARK: - Metrics (Counters) - fun increment(key: String, tags: Map = emptyMap()) { + fun increment( + key: String, + tags: Map = emptyMap(), + ) { Sentry.metrics().count(key, 1.0) } // MARK: - Logs - fun logInfo(message: String, attributes: Map = emptyMap()) { + fun logInfo( + message: String, + attributes: Map = emptyMap(), + ) { Sentry.logger().info(message, attributes) } - fun logWarning(message: String, attributes: Map = emptyMap()) { + fun logWarning( + message: String, + attributes: Map = emptyMap(), + ) { Sentry.logger().warn(message, attributes) } - fun logError(message: String, attributes: Map = emptyMap()) { + fun logError( + message: String, + attributes: Map = emptyMap(), + ) { Sentry.logger().error(message, attributes) } @@ -94,21 +102,34 @@ internal object VirtusizeSentryTracker { // MARK: - Product Check - fun trackProductCheck(externalProductId: String, isValid: Boolean, storeId: String? = null) { - val tags = buildTags(storeId = storeId) + - mapOf("external_product_id" to externalProductId, "is_valid" to isValid.toString()) + fun trackProductCheck( + externalProductId: String, + isValid: Boolean, + storeId: String? = null, + ) { + val tags = + buildTags(storeId = storeId) + + mapOf("external_product_id" to externalProductId, "is_valid" to isValid.toString()) logInfo("product-check", tags) } - fun trackLoadCancelled(step: String, externalProductId: String, storeId: String? = null) { - val tags = buildTags(storeId = storeId) + - mapOf("external_product_id" to externalProductId, "step" to step) + fun trackLoadCancelled( + step: String, + externalProductId: String, + storeId: String? = null, + ) { + val tags = + buildTags(storeId = storeId) + + mapOf("external_product_id" to externalProductId, "step" to step) logWarning("load-cancelled", tags) } // MARK: - Order - fun trackSendOrder(order: VirtusizeOrder, storeId: String? = null) { + fun trackSendOrder( + order: VirtusizeOrder, + storeId: String? = null, + ) { val externalProductIds = order.items.mapNotNull { it.paramsToMap()["externalProductId"] as? String } if (externalProductIds.isEmpty()) { val tags = buildTags(storeId = storeId) @@ -129,8 +150,9 @@ internal object VirtusizeSentryTracker { throwable: Throwable, storeId: String? = null, ) { - val tags = buildTags(storeId = storeId) + - mapOf("error_type" to throwable::class.java.simpleName) + val tags = + buildTags(storeId = storeId) + + mapOf("error_type" to throwable::class.java.simpleName) increment("error", tags) logError(throwable.localizedMessage ?: throwable.message ?: throwable::class.java.simpleName, tags) } From 5813ef1f6ce35fc6849062bc3709df491ed8977e Mon Sep 17 00:00:00 2001 From: OleS Date: Tue, 10 Mar 2026 16:04:31 +0200 Subject: [PATCH 6/6] Unit-Test fix --- .../res/font/subset_noto_sans_jp_bold.ttf | Bin 200488 -> 200584 bytes .../res/font/subset_noto_sans_jp_regular.ttf | Bin 199684 -> 199780 bytes .../res/font/subset_noto_sans_kr_bold.ttf | Bin 105376 -> 105512 bytes .../res/font/subset_noto_sans_kr_regular.ttf | Bin 105332 -> 105468 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/virtusize/src/main/res/font/subset_noto_sans_jp_bold.ttf b/virtusize/src/main/res/font/subset_noto_sans_jp_bold.ttf index e81af91df80f98c71e7404c55953d24ef5f0caf9..f92f74a4344aad8ac0ad2da93b245912a0b845d3 100644 GIT binary patch delta 4716 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 diff --git a/virtusize/src/main/res/font/subset_noto_sans_jp_regular.ttf b/virtusize/src/main/res/font/subset_noto_sans_jp_regular.ttf index 7ff60ea998865c1a95f343b09a5b581b6fadd1bc..37c036be95254ce09a0f7660421e1cf47c62754f 100644 GIT binary patch delta 4279 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 delta 4220 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 diff --git a/virtusize/src/main/res/font/subset_noto_sans_kr_bold.ttf b/virtusize/src/main/res/font/subset_noto_sans_kr_bold.ttf index 12ad4a9651a37811ceebb1f5f2ae8efc554641d4..033b6851a7ce0a8ed1ada880e4835dbc6b36b809 100644 GIT binary patch delta 2795 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 J+8~PM{{S1Y;A8** diff --git a/virtusize/src/main/res/font/subset_noto_sans_kr_regular.ttf b/virtusize/src/main/res/font/subset_noto_sans_kr_regular.ttf index 7202223bfa7c73503254d74b71b9749278187811..a3cef382af4a67a3bb19c48390242e70a2a774dd 100644 GIT binary patch delta 2727 zcmYLLc~lek7XHmdQGwQ?xG#VpYgUp#G82+umdQ3rNMsQO2|Kb1$fgkXO%@?QL{U(I zuqqZ6^wqkxwa;3&7HO6CY5ORZwzh7q_9?u9_s2VP=HB_f-*>k;XXf0O=d9k2Sh)iT z0ANR$1>gfrtI@|=0v!QlJOhAzHKq^TqQ39A6F^PE2KSA|79pXfMkO24YOAdp0I(eb z;Qi7#Lq;0Ge?UYH5Por7Qf}-(w(bD{j-3G7aUnj&5WP8+R|WvIo;df3Cy2$^LOp@E z64)g^IWy;D$DRWOehq;2xun!6!}W~R9|2g$17KlnvLPo8z6SOHBY<*<0LK(VatsB* z2z`D80I+9TYDVU4Iq)VLsF^!YOOHv5x2houu455+Q5I3qD%&myKrM5Px?~Xr_bE!R z5V&cs!2BzKA*;K5-L0KVd9MJ8^fRFY6GP6_IX^iz_TKE*Gfzxnh`FeDP7QxJTmU_R zhocrmEVdzXk`BVe;Lt(SE!!w)YMB0BD+!u1(ccpd+t|62NY&|qCLenb~X( zYOS5tN;9nK({yXvG_9IqjZ)*SzN)^Y{zx6DM%2D)H}yg_34e`0#ec=8@gMO=_yfEG zFU2$Q6g(c^V8mDA)~cJTf2uxHT~f8G8daEzt+G?Sud-2*R6zN?@`mygWuH>7)GGOk zX@!lvP@XSOm#4}TLMP!y;Znh`f@#5&QSeYuB#0G62|@%x zf`$Bl@hAA-^6&BQ@(20-{2u->ek-qohw~IXIZwv(=LvWykIQr5*`m|v6gr81kA8!W zq4&_w(aY$^=s(a?XfIla?njMi0h)zopwVa;>WVHyZBc7<5ekqQR&aBHNH4M33Nz5>X%$M1XJ*HbUc0a365TxwpADxu0?SxtZME+*ocDH=OIr z`JM9{=P~DdPCUoJ3FQQFSR8K-jpN2~;VkEndo3)D-%Zgxmu-<3BFfuPP&oNIiJDF8XAEq1A+WUt0b;cdWS;mKqQw$5Eh>^!g zW5hFJ89@v!V+(zj{+9lR{+vEV|CZiDH_=OIM`*>gLK;p}&=4An#-LGYuC(>EwbWD8 z25J#?FExT1PTfv?zr92;`rEUqf?YqmD72rXPcZi$v5R~I_nIa15$Q*Je`5gHIx&Q9hzfQ#Vs{se{yMnl~+&R!1ABThnFqI{IzK za)u8hnbE=c%iG@D&pX-skoN?00W+9+h&jcgvW%>6*m8CS`<4&n@bKa1Z4 ze+567-^jls*d)jlGz+c?-GsryPT_O^b^f@2nt!|hnE%Wc_bofN^obUV+(mj(wdjHv z61#}?;%afX__p{z#J9FrGAQ{<3Q4`CG14~aFW5>V{+Ezw>LunW)xm~|_3I$f*o2R%#QpdZ`1d~4X&zChbRLtty*cR?OOsX?Q`_Q63qm9zl_6u>+_%MT8xCcKR)&smN4FaQ!56Lu$D-sQ3@Z`XJtGqE{wGKrEDmo%94Dw&^Lo_r}~K}tZ%{nU-A38|B5%CzdV z+s54vyAyVwOW%+lk=~v@nL*7c$heuw$lQ~8A#-LAzNcf)pIM<$65Ua(ibw|}2t-%$R>{HXks`7;Fx1>;6HW1?}i@Poq8!p_3! zB27_y(R4AlxT*L_iKfI-@}^W#T3z~8*|IWYblIu0AImqC2b4FK->>kgh_C3Zn5bM* zDXh$|JYP9gGmm6#v6b+3H4;#@&W8ZBtIu zfU#++d3AGG^Ki?OmcW*-me*$7eBAu^!PtYNt#+*)hinh+J9Pc<+QW&5ueLGUjBTGE zS$|~LkFm$oaRE4RzkHQ05h>$|QO-Eg;i_xA3VZq(8}+Wp3YSRyTDmLAJgk84jzPijwF z&!e8%Uf3GRna!b9dP6XfL70~4zm z0Z)U4M8cBU3-k7qdD}f_&7&sh6_js2*#tS@MMN?TDB$9Gj}x=N0E3N$RS>Z{lYl^gC?XEb z2vp>?REtwv93D@fv@WZn*4L^_iq=;2X>Emfp+DY_v-jHj?BP3mowe8Z*SPtcFU&mw z5C8xhT$lpZs9F^gXV6*!AVviM+gdb48>l?*qyRv}M3bij);fX5l4Fu}Y32owasa?; z2mn?W=yjQCxZfJj^2D`;J}ED5_K0Js^y_l?IL7O0s9 zx3}T8TYPd>?z!szNZfu80G4NxQe$-PHU*miU=xnQY)`T-Hx2q4*au($n2RSkrRb7l zNid4X7d!v}$U7}HGi$LD@WBCCEZwJN#HPiY*Hz#JmH>T{jW;yUu?Yph))z|+v+;%% z75ldWV4L+)gXIxGp!rU_yH9ql;k*S*OrGO$z=PASUZd0O)(C@t4E`Zlubx#Ws6*8(wU^pK zZLhXgo2kyI22?$&BdSBH5|va%Q(jhHP=2M{rerI9l^#k{C8T(#c&V6E%qo6T{HVC6 zs8p0GG8M@Ry<)RsJ*HS8pORmbPs%UI56PS4A~{2DEng+KltZ#b*+bcN**CHQnMS6P z`AcV|mJ&=-AW4^`Na7{25{1NGVk=${|0RAZ{#krmd`5g)+$|P~3FriR0_{YPpe<+} znu;c)31|!&jq1>yXc)R3RiaW`$Kx8Tcg$u$U zAz#Q9QiT*DNk|ZS3AYICh3f@Z1m6nI3O*C`3t9yRL8G8t5G~j(h!kjpW`gbo-3bZ_ zk_T-Gf&vWuFZo~ad-=!to%{yAny=)`_yRtk5A*4KC;k@xYTg`gmiLTzALAACVtLWL z?K~~dl>3M~!@bL$=HB8CaR<45+~eFsoMRk0N5T!2!Dj%!mr^O_%?hQ z?u8G))i7287r~iu7_5agupE}c5?BCpVLzAwli4%ud+a;xo9rp}BzusZ$xdU(vZL9N zYQoZzNh|9eMY@YZKXC-ODP?c5(-9?D>EHsw+X;vD5aKO|j>W zT*m^(D~|7+T%1yz`kkISdpoP0i=Dr8{>8=GCBUV?<*KW>tHjmd`pzxD?TFiRcanR) z`?&jphlfYB$6=4F9`jp0w-#^x%5$wJ-*dm`i03UYORrF`9Iq~~yIzal8@&16nD+_q z8{P{9GC@aZAWRY#i3qWfc$WB%q{c{Pq*2lfGM}799wq-laiC}^os?LXNmAiU8BAmY!@6A{6Mn-Uv2}MKSRRsC3t_E+qU#=-)lX!yS3fg-$H#uvCx_AtG7$G zS8SgOvkFs%b%ebNCx_>U|G7i8qjM*;vwSD^Dnc4@IO1U>A+kO4ew2GuM%3i4wY#Es zP48yz{zB)di`9)s+e8;de;Z>P6Bl!74{cAuo{3nC*d4JK;?~7w=~w7u^+WM?@!I&# z_+R$2_EznkO9)8lO!$x(kvNq2CW)Msl{ArTmaIswOrA>lH05Z@(^P8eP}ITnZis%=KCy8R!P?6KHGizedn{CvzxLX<+$dgg!K8!ZjX)!>v8wUACYz@4rjyN3v#2?{d8B#P;BC+uP8uG!*thUnGFwKl zmLFT)TJu|PwQX)oYP)E(HSRTDK14c%9lCp1fB5_Mp!SCL{~Yl-Qgh@(hp6LV$InM; zN9&Kyb;>(GKjw7|JNCzM(ecXTH&4({RG+wca?8oElg%e@bUAiqbai!|>AKt<-u@s(A<+3K?n{W)8;vr<Du?=@erNzX1r0$X&}riX1GvF#5E_O)Gk!3D&dz<% zDV#zKL#F{VJiB!9+_L<3S@teTBc}y?>tBFV4^#YrfCYYhm$eyS1=s?c0T;j%Dh5md z6p%tC%UXfI2m(;32$xa_Gp4kH_M7T(0|ab