From 2a2038837a1d48228005fae1934499860946425e Mon Sep 17 00:00:00 2001 From: Michelle Dayangco Date: Fri, 13 Feb 2026 17:56:54 +0800 Subject: [PATCH 1/3] Integrate sentry --- Virtusize.xcodeproj/project.pbxproj | 50 +++++++++++++++++-- Virtusize/Sources/Info.plist | 4 ++ .../Models/VirtusizeParamsBuilder.swift | 17 +++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/Virtusize.xcodeproj/project.pbxproj b/Virtusize.xcodeproj/project.pbxproj index c0286f51..9ce4ad7e 100644 --- a/Virtusize.xcodeproj/project.pbxproj +++ b/Virtusize.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ 52D1E09B2D6DCEA90073737B /* I18nTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D1E09A2D6DCEA30073737B /* I18nTests.swift */; }; 52D1E09E2D6DD09A0073737B /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D1E09D2D6DD0940073737B /* MockURLSession.swift */; }; 52D1E0A02D6DD5900073737B /* I18nFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D1E09F2D6DD58A0073737B /* I18nFixtures.swift */; }; - 52FDA2882D3A5C8F007F5AC8 /* VirtusizeAuth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FDA2872D3A5C8F007F5AC8 /* VirtusizeAuth.framework */; }; 741DD0692DA3D06B007DF2C3 /* VirtusizeFlutter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741DD0682DA3D06B007DF2C3 /* VirtusizeFlutter.swift */; }; 741DD06D2DA42E63007DF2C3 /* VirtusizeFlutterProductEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741DD06C2DA42E63007DF2C3 /* VirtusizeFlutterProductEventHandler.swift */; }; 748DF81A2E3FF2A9002CA7FC /* VirtusizeGetSizeParamsShoe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748DF8192E3FF2A9002CA7FC /* VirtusizeGetSizeParamsShoe.swift */; }; @@ -77,7 +76,6 @@ 9C787FCD25DB7A3D00346F2A /* SizeRecommendationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C787FCC25DB7A3D00346F2A /* SizeRecommendationType.swift */; }; 9C84B3FD26C2566B00DDF434 /* VirtusizeViewEventProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C84B3FC26C2566B00DDF434 /* VirtusizeViewEventProtocol.swift */; }; 9C84B40126C276CA00DDF434 /* VirtusizeNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C84B40026C276CA00DDF434 /* VirtusizeNotification.swift */; }; - 9C92B3A52CB2DF7F001DD4A2 /* VirtusizeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C92B3A42CB2DF7F001DD4A2 /* VirtusizeCore.framework */; }; 9C969FD124EB5F8A00DD642F /* VirtusizeI18nLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C969FD024EB5F8A00DD642F /* VirtusizeI18nLocalization.swift */; }; 9C969FD324EBE0C400DD642F /* DeserializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C969FD224EBE0C300DD642F /* DeserializerTests.swift */; }; 9C969FD824EBF71200DD642F /* i18n_en.json in Resources */ = {isa = PBXBuildFile; fileRef = 9C969FD524EBF71200DD642F /* i18n_en.json */; }; @@ -119,8 +117,13 @@ 9CFF40FC25B974D200B21D4E /* VirtusizeViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFF40FB25B974D200B21D4E /* VirtusizeViewStyle.swift */; }; 9CFF410D25B98D4E00B21D4E /* VirtusizeInPageStandardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFF410C25B98D4E00B21D4E /* VirtusizeInPageStandardViewModel.swift */; }; 9CFF411325B98FA900B21D4E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFF411225B98FA900B21D4E /* Observable.swift */; }; + D760129E2F3F2A1700AF72AA /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = D760129D2F3F2A1700AF72AA /* Sentry */; }; D7BC74B42EDDD063004616BF /* product_types.json in Resources */ = {isa = PBXBuildFile; fileRef = D7BC74B32EDDD063004616BF /* product_types.json */; }; D7BC74B52EDDD063004616BF /* product_types.json in Resources */ = {isa = PBXBuildFile; fileRef = D7BC74B32EDDD063004616BF /* product_types.json */; }; + D7C67BE72F3DC56E0007167B /* VirtusizeAuth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FDA2872D3A5C8F007F5AC8 /* VirtusizeAuth.framework */; }; + D7C67BE82F3DC56E0007167B /* VirtusizeAuth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 52FDA2872D3A5C8F007F5AC8 /* VirtusizeAuth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D7C67BEA2F3DC5700007167B /* VirtusizeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C92B3A42CB2DF7F001DD4A2 /* VirtusizeCore.framework */; }; + D7C67BEB2F3DC5700007167B /* VirtusizeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9C92B3A42CB2DF7F001DD4A2 /* VirtusizeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DF71642A21A28858002C7202 /* VirtusizeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF71642921A28858002C7202 /* VirtusizeEvent.swift */; }; DF71642C21A28939002C7202 /* VirtusizeEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF71642B21A28939002C7202 /* VirtusizeEnvironment.swift */; }; DF71642E21A28DE8002C7202 /* VirtusizeProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF71642D21A28DE8002C7202 /* VirtusizeProduct.swift */; }; @@ -139,6 +142,21 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + D7C67BE92F3DC56E0007167B /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D7C67BE82F3DC56E0007167B /* VirtusizeAuth.framework in Embed Frameworks */, + D7C67BEB2F3DC5700007167B /* VirtusizeCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 4D6C5799205FCBE900DA910E /* VirtusizeAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeAPIRequest.swift; sourceTree = ""; }; 4DED8D1C205755C3001CA7DF /* Virtusize.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Virtusize.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -269,8 +287,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 52FDA2882D3A5C8F007F5AC8 /* VirtusizeAuth.framework in Frameworks */, - 9C92B3A52CB2DF7F001DD4A2 /* VirtusizeCore.framework in Frameworks */, + D7C67BE72F3DC56E0007167B /* VirtusizeAuth.framework in Frameworks */, + D7C67BEA2F3DC5700007167B /* VirtusizeCore.framework in Frameworks */, + D760129E2F3F2A1700AF72AA /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -646,6 +665,7 @@ 4DED8D19205755C3001CA7DF /* Headers */, 4DED8D1A205755C3001CA7DF /* Resources */, DF6B5C91219BD262000B3402 /* ShellScript */, + D7C67BE92F3DC56E0007167B /* Embed Frameworks */, ); buildRules = ( ); @@ -706,6 +726,9 @@ Base, ); mainGroup = 4DED8D12205755C3001CA7DF; + packageReferences = ( + D760129C2F3F2A1700AF72AA /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + ); productRefGroup = 4DED8D1D205755C3001CA7DF /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1180,6 +1203,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D760129C2F3F2A1700AF72AA /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.4.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D760129D2F3F2A1700AF72AA /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = D760129C2F3F2A1700AF72AA /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4DED8D13205755C3001CA7DF /* Project object */; } diff --git a/Virtusize/Sources/Info.plist b/Virtusize/Sources/Info.plist index fcd36391..c2662653 100644 --- a/Virtusize/Sources/Info.plist +++ b/Virtusize/Sources/Info.plist @@ -20,5 +20,9 @@ $(CURRENT_PROJECT_VERSION) NSPrincipalClass + SentryDSN + https://f2ae6aac72a9ec47631fdbc4cd8589e6@o903.ingest.us.sentry.io/4510877679353856 + SentryTracesSampleRate + 1 diff --git a/Virtusize/Sources/Models/VirtusizeParamsBuilder.swift b/Virtusize/Sources/Models/VirtusizeParamsBuilder.swift index f7b0de3a..dd9ed452 100644 --- a/Virtusize/Sources/Models/VirtusizeParamsBuilder.swift +++ b/Virtusize/Sources/Models/VirtusizeParamsBuilder.swift @@ -22,6 +22,7 @@ // THE SOFTWARE. // +import Sentry /// The builder patten to help initialize the `VirtusizeParams` object public class VirtusizeParamsBuilder { private var region: VirtusizeRegion = VirtusizeRegion.JAPAN @@ -76,10 +77,26 @@ public class VirtusizeParamsBuilder { serviceEnvironment = value return self } + + private func initSentry(){ + let moduleBundle = Bundle(for: Virtusize.self) + + if let dsn = moduleBundle.object(forInfoDictionaryKey: "SentryDSN") as? String { + SentrySDK.start { options in + options.dsn = dsn + options.tracesSampleRate = moduleBundle.object(forInfoDictionaryKey: "SentryTracesSampleRate") as? NSNumber ?? 1.0 + options.debug = true + } + } + } + + public func build() -> VirtusizeParams { /// Assigns the region value to a default one corresponding the Virtusize environment region = Virtusize.environment.virtusizeRegion() + initSentry() + return VirtusizeParams( region: region, language: language ?? region.defaultLanguage(), From c980f046f6773c34f93878273333343b66d09ffa Mon Sep 17 00:00:00 2001 From: OleS Date: Tue, 24 Feb 2026 01:55:36 +0200 Subject: [PATCH 2/3] Add Sentry logs + metrics to SDK Sentry Logs: - product check - API requests - SDK errors - WebView events - Request product cancellation Sentry Metrics: - user saw product counter - send order counter - SDK errors counter --- Package.swift | 13 +- Virtusize.podspec | 1 + Virtusize.xcodeproj/project.pbxproj | 4 + .../Internal/API/VirtusizeAPIService.swift | 1 - .../Internal/DefaultEventHandler.swift | 63 +++++++ .../Internal/VirtusizeSentryTracker.swift | 160 ++++++++++++++++++ .../Sources/Models/VirtusizeOrderItem.swift | 2 +- .../UI/VirtusizeWebViewController.swift | 2 +- Virtusize/Sources/Virtusize.swift | 50 +++++- 9 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 Virtusize/Sources/Internal/VirtusizeSentryTracker.swift diff --git a/Package.swift b/Package.swift index 937e7d25..e6fd75fb 100644 --- a/Package.swift +++ b/Package.swift @@ -19,11 +19,20 @@ let package = Package( targets: ["VirtusizeCore"] ) ], - + dependencies: [ + .package( + url: "https://github.com/getsentry/sentry-cocoa.git", + from: "8.36.0" + ) + ], targets: [ .target( name: "Virtusize", - dependencies: ["VirtusizeCore", "VirtusizeAuth"], + dependencies: [ + "VirtusizeCore", + "VirtusizeAuth", + .product(name: "Sentry", package: "sentry-cocoa") + ], path: "Virtusize/Sources", exclude: ["Info.plist"], resources: [.process("Resources")] diff --git a/Virtusize.podspec b/Virtusize.podspec index 83e31eff..58eca55f 100644 --- a/Virtusize.podspec +++ b/Virtusize.podspec @@ -18,6 +18,7 @@ Pod::Spec.new do |s| s.resource_bundle = { 'Virtusize' => ["Virtusize/Sources/Resources/**/*.lproj", "Virtusize/Sources/Resources/PrivacyInfo.xcprivacy"] } s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } + s.dependency 'Sentry', '~> 8.36' s.subspec 'VirtusizeCore' do |ss| ss.dependency 'VirtusizeCore', "#{s.version}" end diff --git a/Virtusize.xcodeproj/project.pbxproj b/Virtusize.xcodeproj/project.pbxproj index 9ce4ad7e..5b5a2c5a 100644 --- a/Virtusize.xcodeproj/project.pbxproj +++ b/Virtusize.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 74F8E3D02E0EA477002CAE98 /* UIImageViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F8E3CF2E0EA473002CAE98 /* UIImageViewExtensions.swift */; }; 8455FFD42B29A0C500019B1B /* VirtusizeGetSizeItemsParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455FFD32B29A0C500019B1B /* VirtusizeGetSizeItemsParam.swift */; }; 84CF03B92BD7A64A00C08920 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 84CF03B82BD7A64A00C08920 /* PrivacyInfo.xcprivacy */; }; + 9BE02B152F4D013000083151 /* VirtusizeSentryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE02B142F4D013000083151 /* VirtusizeSentryTracker.swift */; }; 9C0EBA2225BACADD00F1D746 /* VirtusizeProductImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0EBA2125BACADD00F1D746 /* VirtusizeProductImage.swift */; }; 9C106EB024FCE0C70012EE51 /* VirtusizeInPageStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C106EAF24FCE0C70012EE51 /* VirtusizeInPageStandard.swift */; }; 9C106EB424FE41EC0012EE51 /* VirtusizeProductImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C106EB324FE41EC0012EE51 /* VirtusizeProductImageView.swift */; }; @@ -185,6 +186,7 @@ 74F8E3CF2E0EA473002CAE98 /* UIImageViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageViewExtensions.swift; sourceTree = ""; }; 8455FFD32B29A0C500019B1B /* VirtusizeGetSizeItemsParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeGetSizeItemsParam.swift; sourceTree = ""; }; 84CF03B82BD7A64A00C08920 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 9BE02B142F4D013000083151 /* VirtusizeSentryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeSentryTracker.swift; sourceTree = ""; }; 9C0EBA2125BACADD00F1D746 /* VirtusizeProductImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeProductImage.swift; sourceTree = ""; }; 9C106EAF24FCE0C70012EE51 /* VirtusizeInPageStandard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeInPageStandard.swift; sourceTree = ""; }; 9C106EB324FE41EC0012EE51 /* VirtusizeProductImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtusizeProductImageView.swift; sourceTree = ""; }; @@ -306,6 +308,7 @@ 4D6C5794205FB9FC00DA910E /* Internal */ = { isa = PBXGroup; children = ( + 9BE02B142F4D013000083151 /* VirtusizeSentryTracker.swift */, 52ACB08F2D7083C60011E51E /* DefaultEventHandler.swift */, 9CEC8D88252AB8EE001CBDFB /* Models */, 9C106EB524FE424E0012EE51 /* Views */, @@ -841,6 +844,7 @@ 9C32A14B250279D00062C11D /* VirtusizeInPageView.swift in Sources */, 9C787FC925DB793400346F2A /* VirtusizeEventHandler.swift in Sources */, 9CFF40FC25B974D200B21D4E /* VirtusizeViewStyle.swift in Sources */, + 9BE02B152F4D013000083151 /* VirtusizeSentryTracker.swift in Sources */, 52ACB0902D7083D10011E51E /* DefaultEventHandler.swift in Sources */, 9CB98B9D24B2C5B400C51F20 /* VirtusizeLanguage.swift in Sources */, 9C5956CD2484F6280014DF51 /* VirtusizeOrderItem.swift in Sources */, diff --git a/Virtusize/Sources/Internal/API/VirtusizeAPIService.swift b/Virtusize/Sources/Internal/API/VirtusizeAPIService.swift index 0e0c8e29..fbcb0dda 100644 --- a/Virtusize/Sources/Internal/API/VirtusizeAPIService.swift +++ b/Virtusize/Sources/Internal/API/VirtusizeAPIService.swift @@ -75,7 +75,6 @@ class VirtusizeAPIService: APIService { guard let request = APIRequest.sendEvent(event, withContext: context) else { return nil } - let response = await VirtusizeAPIService.performAsync(request) guard response.virtusizeError == nil, let data = response.data else { // failed to perform request diff --git a/Virtusize/Sources/Internal/DefaultEventHandler.swift b/Virtusize/Sources/Internal/DefaultEventHandler.swift index ff721767..1c516743 100644 --- a/Virtusize/Sources/Internal/DefaultEventHandler.swift +++ b/Virtusize/Sources/Internal/DefaultEventHandler.swift @@ -23,50 +23,113 @@ // THE SOFTWARE. // +import VirtusizeCore + internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventProtocol { var virtusizeEventHandler: VirtusizeEventHandler? + private var sentryStoreId: String? { + APICache.shared.currentStoreId.map { String($0) } + } + public func userOpenedWidget() { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userOpenedWidget.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserOpenedWidget() } public func userAuthData(bid: String?, auth: String?) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userAuthData.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserAuthData(bid: bid, auth: auth) } public func userSelectedProduct(userProductId: Int?) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userSelectedProduct.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserSelectedProduct(userProductId: userProductId) } public func userAddedProduct() { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userAddedProduct.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserAddedProduct() } public func userDeletedProduct(userProductId: Int?) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userDeletedProduct.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserDeletedProduct(userProductId: userProductId) } public func userChangedRecommendationType(changedType: SizeRecommendationType?) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userChangedRecommendationType.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserChangedRecommendationType(changedType: changedType) } public func userUpdatedBodyMeasurements(recommendedSize: String?) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userUpdatedBodyMeasurements.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserUpdatedBodyMeasurements(recommendedSize: recommendedSize) } public func userLoggedIn() { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userLoggedIn.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserLoggedIn() } public func clearUserData() { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: "user-clear-data", + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) + VirtusizeSentryTracker.trackSessionEnd(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId) handleClearUserData() } public func userClosedWidget() { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userClosedWidget.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) + VirtusizeSentryTracker.trackSessionEnd(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId) handleUserClosedWidget() } public func userClickedLanguageSelector(language: VirtusizeLanguage) { + VirtusizeSentryTracker.trackWebViewEvent( + eventName: VirtusizeEventName.userClickedLanguageSelector.rawValue, + sessionId: VirtusizeSentryTracker.currentSessionId, + storeId: sentryStoreId + ) handleUserClickedLanguageSelector(language: language) } } diff --git a/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift b/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift new file mode 100644 index 00000000..e806d344 --- /dev/null +++ b/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift @@ -0,0 +1,160 @@ +// +// VirtusizeSentryTracker.swift +// +// Copyright (c) 2018-present Virtusize KK +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import Sentry + +/// Utility for Sentry metrics and structured logs tracking in the Virtusize SDK. +internal enum VirtusizeSentryTracker { + + // MARK: - Session Management + + /// The current active session ID, set by Virtusize.load. + static var currentSessionId: String = "" + + /// Generates a new UUID session ID, stores it as the current session, and returns it. + @discardableResult + static func generateSessionId() -> String { + currentSessionId = UUID().uuidString + return currentSessionId + } + + // MARK: - Metrics (Counters) + + static func increment(_ key: String, tags: [String: String] = [:]) { + SentrySDK.metrics.count( + key: key, + value: 1, + attributes: tags + ) + } + + // MARK: - Logs + + static func logInfo(_ message: String, attributes: [String: String] = [:]) { + SentrySDK.logger.info(message, attributes: attributes) + } + + static func logWarning(_ message: String, attributes: [String: String] = [:]) { + SentrySDK.logger.warn(message, attributes: attributes) + } + + static func logError(_ message: String, attributes: [String: String] = [:]) { + SentrySDK.logger.error(message, attributes: attributes) + } + + // MARK: - WebView Session + + static func trackSessionStart(sessionId: String, storeId: String? = nil) { + var tags = ["session_id": sessionId] + if let storeId { tags["store_id"] = storeId } + + logInfo("webview-session-start", attributes: tags) + } + + static func trackSessionEnd(sessionId: String, storeId: String? = nil) { + var tags = ["session_id": sessionId] + if let storeId { tags["store_id"] = storeId } + + logInfo("webview-ession-end", attributes: tags) + } + + // MARK: - Product Check + + static func trackProductCheck(externalProductId: String, isValid: Bool, storeId: String? = nil) { + var tags: [String: String] = ["external_product_id": externalProductId, "is_valid": String(isValid)] + if let storeId { tags["store_id"] = storeId } + logInfo("product-check", attributes: tags) + } + + static func trackLoadCancelled(step: String, externalProductId: String, storeId: String? = nil) { + var tags: [String: String] = ["external_product_id": externalProductId, "step": step] + if let storeId { tags["store_id"] = storeId } + logWarning("load-cancelled", attributes: tags) + } + + // MARK: - WebView Events + + static func trackWebViewEvent( + eventName: String, + sessionId: String, + storeId: String? = nil + ) { + var tags = ["event_name": eventName, "session_id": sessionId] + if let storeId { tags["store_id"] = storeId } + + logInfo("webview-`\(eventName)`" , attributes: tags) + } + + static func trackUserSawProduct(externalProductId: String? = nil, storeId: String? = nil) { + var tags: [String: String] = [:] + if let storeId { tags["store_id"] = storeId } + if let externalProductId { tags["external_product_id"] = externalProductId } + + increment("user.saw.product", tags: tags) + logInfo("user-saw-product", attributes: tags) + } + + // MARK: - Order + + static func trackSendOrder(order: VirtusizeOrder, storeId: String? = nil) { + var baseTags: [String: String] = [:] + if let storeId { baseTags["store_id"] = storeId } + if order.items.isEmpty { + increment("order.sent", tags: baseTags) + } else { + for item in order.items { + var tags = baseTags + tags["external_product_id"] = item.externalProductId + increment("order.sent", tags: tags) + } + } + + logInfo("order-sent", attributes: baseTags) + } + + // MARK: - API Request + + static func trackAPIRequest(endpoint: String, storeId: String? = nil) { + var tags = ["endpoint": endpoint] + if let storeId { tags["store_id"] = storeId } + + logInfo("api-request", attributes: tags) + } + + // MARK: - Error + + static func trackError( + _ error: Error, + sessionId: String? = nil, + storeId: String? = nil + ) { + var tags = ["error_type": String(describing: type(of: error))] + if let sessionId { tags["session_id"] = sessionId } + if let storeId { tags["store_id"] = storeId } + + increment("error", tags: tags) + logError(error.localizedDescription, attributes: tags) + } +} diff --git a/Virtusize/Sources/Models/VirtusizeOrderItem.swift b/Virtusize/Sources/Models/VirtusizeOrderItem.swift index c49be495..ab0e5e02 100644 --- a/Virtusize/Sources/Models/VirtusizeOrderItem.swift +++ b/Virtusize/Sources/Models/VirtusizeOrderItem.swift @@ -25,7 +25,7 @@ /// This structure wraps the parameters of the order item for the API request of sending the order public struct VirtusizeOrderItem: Codable { /// The external product ID provided by the client. It must be unique for a product. - private let externalProductId: String + internal let externalProductId: String /// The name of the size, e.g. "S", "M", etc. private let size: String /// The alias of the size is added if the size name is not identical from the product page diff --git a/Virtusize/Sources/UI/VirtusizeWebViewController.swift b/Virtusize/Sources/UI/VirtusizeWebViewController.swift index 64a56ec3..0ff60b8d 100644 --- a/Virtusize/Sources/UI/VirtusizeWebViewController.swift +++ b/Virtusize/Sources/UI/VirtusizeWebViewController.swift @@ -47,7 +47,7 @@ public final class VirtusizeWebViewController: UIViewController { // Allow process pool passing to share cookies private var processPool: WKProcessPool? - + // Close button and timer properties private var closeButton: UIButton? private var closeButtonTimer: Timer? diff --git a/Virtusize/Sources/Virtusize.swift b/Virtusize/Sources/Virtusize.swift index ee44f832..739729cd 100644 --- a/Virtusize/Sources/Virtusize.swift +++ b/Virtusize/Sources/Virtusize.swift @@ -116,19 +116,41 @@ public class Virtusize { // Create new task let task = Task { // Check if cancelled early - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "start", externalProductId: product.externalId) + return + } let productWithPDCData = await virtusizeRepository.checkProductValidity(product: product) - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "product-check", externalProductId: product.externalId) + return + } guard let productWithPDCData = productWithPDCData else { + VirtusizeSentryTracker.trackProductCheck(externalProductId: product.externalId, isValid: false) + VirtusizeSentryTracker.trackError( + NSError(domain: "Virtusize", code: 0, userInfo: [NSLocalizedDescriptionKey: "Product check failed"]), + storeId: nil + ) inPageError = (true, product.externalId) return } + let storeId = productWithPDCData.productCheckData.map { String($0.storeId) } + let isValidProduct = productWithPDCData.productCheckData?.validProduct ?? true + VirtusizeSentryTracker.trackProductCheck(externalProductId: product.externalId, isValid: isValidProduct, storeId: storeId) + + VirtusizeSentryTracker.generateSessionId() + VirtusizeSentryTracker.trackSessionStart(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: storeId) + VirtusizeSentryTracker.trackUserSawProduct(externalProductId: product.externalId, storeId: storeId) + await virtusizeRepository.updateUserSession() - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "user-session", externalProductId: product.externalId, storeId: storeId) + return + } await MainActor.run { NotificationCenter.default.post( name: .productCheckData, @@ -142,8 +164,15 @@ public class Virtusize { productId: productWithPDCData.productCheckData?.productDataId ) - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "fetch-initial-data", externalProductId: product.externalId, storeId: storeId) + return + } guard let serverProduct = serverProduct else { + VirtusizeSentryTracker.trackError( + NSError(domain: "Virtusize", code: 0, userInfo: [NSLocalizedDescriptionKey: "Fetch initial data failed"]), + storeId: storeId + ) inPageError = (true, product.externalId) return } @@ -156,10 +185,16 @@ public class Virtusize { ) } - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "store-product", externalProductId: product.externalId, storeId: storeId) + return + } await virtusizeRepository.fetchDataForInPageRecommendation(storeProduct: serverProduct) - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { + VirtusizeSentryTracker.trackLoadCancelled(step: "fetch-recommendations", externalProductId: product.externalId, storeId: storeId) + return + } await MainActor.run { virtusizeRepository.updateInPageRecommendation(product: serverProduct) } @@ -195,9 +230,12 @@ public class Virtusize { ) { Task { do { + let storeId = APICache.shared.currentStoreId.map { String($0) } + VirtusizeSentryTracker.trackSendOrder(order: order, storeId: storeId) try await virtusizeRepository.sendOrder(order) onSuccess?() } catch let error as VirtusizeError { + VirtusizeSentryTracker.trackError(error, storeId: APICache.shared.currentStoreId.map { String($0) }) onError?(error) } } From a7769212d9f77b77845dd5f2fb244082a2127d05 Mon Sep 17 00:00:00 2001 From: OleS Date: Fri, 27 Feb 2026 03:04:57 +0200 Subject: [PATCH 3/3] Codefix --- .../Internal/DefaultEventHandler.swift | 15 ++-------- .../Internal/VirtusizeSentryTracker.swift | 29 +++++-------------- Virtusize/Sources/Virtusize.swift | 1 - 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/Virtusize/Sources/Internal/DefaultEventHandler.swift b/Virtusize/Sources/Internal/DefaultEventHandler.swift index 1c516743..6589a72a 100644 --- a/Virtusize/Sources/Internal/DefaultEventHandler.swift +++ b/Virtusize/Sources/Internal/DefaultEventHandler.swift @@ -35,7 +35,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userOpenedWidget() { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userOpenedWidget.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserOpenedWidget() @@ -44,7 +43,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userAuthData(bid: String?, auth: String?) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userAuthData.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserAuthData(bid: bid, auth: auth) @@ -53,7 +51,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userSelectedProduct(userProductId: Int?) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userSelectedProduct.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserSelectedProduct(userProductId: userProductId) @@ -62,7 +59,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userAddedProduct() { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userAddedProduct.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserAddedProduct() @@ -71,7 +67,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userDeletedProduct(userProductId: Int?) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userDeletedProduct.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserDeletedProduct(userProductId: userProductId) @@ -80,7 +75,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userChangedRecommendationType(changedType: SizeRecommendationType?) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userChangedRecommendationType.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserChangedRecommendationType(changedType: changedType) @@ -89,7 +83,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userUpdatedBodyMeasurements(recommendedSize: String?) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userUpdatedBodyMeasurements.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserUpdatedBodyMeasurements(recommendedSize: recommendedSize) @@ -98,7 +91,6 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func userLoggedIn() { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userLoggedIn.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserLoggedIn() @@ -107,27 +99,24 @@ internal class DefaultEventHandler: VirtusizeEventHandler, VirtusizeViewEventPro public func clearUserData() { VirtusizeSentryTracker.trackWebViewEvent( eventName: "user-clear-data", - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) - VirtusizeSentryTracker.trackSessionEnd(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId) + handleClearUserData() } public func userClosedWidget() { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userClosedWidget.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) - VirtusizeSentryTracker.trackSessionEnd(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId) + handleUserClosedWidget() } public func userClickedLanguageSelector(language: VirtusizeLanguage) { VirtusizeSentryTracker.trackWebViewEvent( eventName: VirtusizeEventName.userClickedLanguageSelector.rawValue, - sessionId: VirtusizeSentryTracker.currentSessionId, storeId: sentryStoreId ) handleUserClickedLanguageSelector(language: language) diff --git a/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift b/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift index e806d344..697e621d 100644 --- a/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift +++ b/Virtusize/Sources/Internal/VirtusizeSentryTracker.swift @@ -34,9 +34,13 @@ internal enum VirtusizeSentryTracker { static 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. @discardableResult static func generateSessionId() -> String { currentSessionId = UUID().uuidString + SentrySDK.configureScope { scope in + scope.setTag(value: currentSessionId, key: "session_id") + } return currentSessionId } @@ -64,33 +68,19 @@ internal enum VirtusizeSentryTracker { SentrySDK.logger.error(message, attributes: attributes) } - // MARK: - WebView Session - - static func trackSessionStart(sessionId: String, storeId: String? = nil) { - var tags = ["session_id": sessionId] - if let storeId { tags["store_id"] = storeId } - - logInfo("webview-session-start", attributes: tags) - } - - static func trackSessionEnd(sessionId: String, storeId: String? = nil) { - var tags = ["session_id": sessionId] - if let storeId { tags["store_id"] = storeId } - - logInfo("webview-ession-end", attributes: tags) - } - // MARK: - Product Check static func trackProductCheck(externalProductId: String, isValid: Bool, storeId: String? = nil) { var tags: [String: String] = ["external_product_id": externalProductId, "is_valid": String(isValid)] if let storeId { tags["store_id"] = storeId } + logInfo("product-check", attributes: tags) } static func trackLoadCancelled(step: String, externalProductId: String, storeId: String? = nil) { var tags: [String: String] = ["external_product_id": externalProductId, "step": step] if let storeId { tags["store_id"] = storeId } + logWarning("load-cancelled", attributes: tags) } @@ -98,13 +88,12 @@ internal enum VirtusizeSentryTracker { static func trackWebViewEvent( eventName: String, - sessionId: String, storeId: String? = nil ) { - var tags = ["event_name": eventName, "session_id": sessionId] + var tags = ["event_name": eventName] if let storeId { tags["store_id"] = storeId } - logInfo("webview-`\(eventName)`" , attributes: tags) + logInfo("webview-\(eventName)", attributes: tags) } static func trackUserSawProduct(externalProductId: String? = nil, storeId: String? = nil) { @@ -147,11 +136,9 @@ internal enum VirtusizeSentryTracker { static func trackError( _ error: Error, - sessionId: String? = nil, storeId: String? = nil ) { var tags = ["error_type": String(describing: type(of: error))] - if let sessionId { tags["session_id"] = sessionId } if let storeId { tags["store_id"] = storeId } increment("error", tags: tags) diff --git a/Virtusize/Sources/Virtusize.swift b/Virtusize/Sources/Virtusize.swift index 739729cd..ed742679 100644 --- a/Virtusize/Sources/Virtusize.swift +++ b/Virtusize/Sources/Virtusize.swift @@ -142,7 +142,6 @@ public class Virtusize { VirtusizeSentryTracker.trackProductCheck(externalProductId: product.externalId, isValid: isValidProduct, storeId: storeId) VirtusizeSentryTracker.generateSessionId() - VirtusizeSentryTracker.trackSessionStart(sessionId: VirtusizeSentryTracker.currentSessionId, storeId: storeId) VirtusizeSentryTracker.trackUserSawProduct(externalProductId: product.externalId, storeId: storeId) await virtusizeRepository.updateUserSession()