From def6b8db4016592dfe58650306a46e3b295bf4ba Mon Sep 17 00:00:00 2001 From: Adwin Ronald Ross Date: Mon, 13 Jan 2025 12:24:24 +0530 Subject: [PATCH] chore(ios): add screen view event (#1721) --- ios/DemoApp/AppDelegate.swift | 2 +- .../Controller/ObjcDetailViewController.m | 1 + ios/DemoApp/Controller/ViewController.swift | 5 ++ ios/MeasureSDK.xcodeproj/project.pbxproj | 8 ++++ ios/MeasureSDK/Config/Config.swift | 2 +- .../CoreData/Entities/EventEntity.swift | 24 +++++++++- ios/MeasureSDK/CoreData/EventStore.swift | 10 ++-- ios/MeasureSDK/Events/EventType.swift | 1 + ios/MeasureSDK/Events/ScreenViewData.swift | 12 +++++ .../Events/UserTriggeredEventCollector.swift | 46 +++++++++++++++++++ ios/MeasureSDK/Exporter/EventSerializer.swift | 17 +++++++ ios/MeasureSDK/Measure.swift | 20 ++++++++ ios/MeasureSDK/MeasureInitializer.swift | 5 ++ ios/MeasureSDK/MeasureInternal.swift | 4 ++ .../MeasureModel.xcdatamodel/contents | 1 + 15 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 ios/MeasureSDK/Events/ScreenViewData.swift create mode 100644 ios/MeasureSDK/Events/UserTriggeredEventCollector.swift diff --git a/ios/DemoApp/AppDelegate.swift b/ios/DemoApp/AppDelegate.swift index 3d1912dd5..55f01b876 100644 --- a/ios/DemoApp/AppDelegate.swift +++ b/ios/DemoApp/AppDelegate.swift @@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - let clientInfo = ClientInfo(apiKey: "msrsh_48153449fa6045685d605a6dcb684cbf42d5b1cdf780cd79bd58a4423ce8b23d_e6b33343", + let clientInfo = ClientInfo(apiKey: "msrsh_38514d61493cf70ce99a11abcb461e9e6d823e2068c7124a0902b745598f7ffb_65ea2c1c", apiUrl: "http://localhost:8080") let config = BaseMeasureConfig(enableLogging: true, trackScreenshotOnCrash: false, diff --git a/ios/DemoApp/Controller/ObjcDetailViewController.m b/ios/DemoApp/Controller/ObjcDetailViewController.m index a8433c57d..2898bf51f 100644 --- a/ios/DemoApp/Controller/ObjcDetailViewController.m +++ b/ios/DemoApp/Controller/ObjcDetailViewController.m @@ -54,6 +54,7 @@ - (void)viewDidLoad { [self setTitle:@"Objc View Controller"]; [self.view addSubview:tableView]; + [[Measure shared] trackScreenView:@"ObjcViewController"]; } // MARK: - Create Table Header with Buttons diff --git a/ios/DemoApp/Controller/ViewController.swift b/ios/DemoApp/Controller/ViewController.swift index cb5d27172..cfcab2a3c 100644 --- a/ios/DemoApp/Controller/ViewController.swift +++ b/ios/DemoApp/Controller/ViewController.swift @@ -53,6 +53,11 @@ import MeasureSDK Measure.shared.trackEvent(name: "custom_event", attributes: attributes, timestamp: nil) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + Measure.shared.trackScreenView("Home") + } + // MARK: - Table Header View with Buttons func createTableHeaderView() -> UIView { diff --git a/ios/MeasureSDK.xcodeproj/project.pbxproj b/ios/MeasureSDK.xcodeproj/project.pbxproj index 7bb8bfc2c..ed28dc5c2 100644 --- a/ios/MeasureSDK.xcodeproj/project.pbxproj +++ b/ios/MeasureSDK.xcodeproj/project.pbxproj @@ -129,6 +129,8 @@ 52BCF2342CB5AC6E003102DF /* GestureCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BCF2332CB5AC6E003102DF /* GestureCollectorTests.swift */; }; 52BCF2352CB65167003102DF /* MockLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287762C8619E0000EE268 /* MockLogger.swift */; }; 52BCF2372CB651C8003102DF /* MockMeasureInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BCF2362CB651C8003102DF /* MockMeasureInitializer.swift */; }; + 52BFED302D2E476E00AC1A06 /* UserTriggeredEventCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BFED2F2D2E476E00AC1A06 /* UserTriggeredEventCollector.swift */; }; + 52BFED322D2E486800AC1A06 /* ScreenViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BFED312D2E486800AC1A06 /* ScreenViewData.swift */; }; 52C03C442CB3FB29002E3C36 /* GestureTargetFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52C03C432CB3FB29002E3C36 /* GestureTargetFinderTests.swift */; }; 52C844B92CB68A2D004BFE71 /* MeasureSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 524CC5BA2C6A4B11001AB506 /* MeasureSDK.framework */; platformFilter = ios; }; 52C844BA2CB68A2D004BFE71 /* MeasureSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 524CC5BA2C6A4B11001AB506 /* MeasureSDK.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -446,6 +448,8 @@ 52BCF2282CB5AA8E003102DF /* MeasureUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeasureUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52BCF2332CB5AC6E003102DF /* GestureCollectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureCollectorTests.swift; sourceTree = ""; }; 52BCF2362CB651C8003102DF /* MockMeasureInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMeasureInitializer.swift; sourceTree = ""; }; + 52BFED2F2D2E476E00AC1A06 /* UserTriggeredEventCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTriggeredEventCollector.swift; sourceTree = ""; }; + 52BFED312D2E486800AC1A06 /* ScreenViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenViewData.swift; sourceTree = ""; }; 52C03C432CB3FB29002E3C36 /* GestureTargetFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureTargetFinderTests.swift; sourceTree = ""; }; 52CC63C02C9C608E00F7CA0A /* CrashDataPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashDataPersistence.swift; sourceTree = ""; }; 52CC63C22C9C609F00F7CA0A /* CrashDataWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashDataWriter.swift; sourceTree = ""; }; @@ -633,6 +637,8 @@ 5202BE752C8B117900A3496E /* Event.swift */, 5202BE762C8B117900A3496E /* EventProcessor.swift */, 5202BE772C8B117900A3496E /* EventType.swift */, + 52BFED312D2E486800AC1A06 /* ScreenViewData.swift */, + 52BFED2F2D2E476E00AC1A06 /* UserTriggeredEventCollector.swift */, ); path = Events; sourceTree = ""; @@ -1430,6 +1436,7 @@ 526E30F62CE77BCB00F484B4 /* AppLaunchEvents.swift in Sources */, 52A853402C994D7900B2A39F /* Exception.swift in Sources */, 5222C9E82D14605000B198DA /* NetworkInterceptor.swift in Sources */, + 52BFED322D2E486800AC1A06 /* ScreenViewData.swift in Sources */, 52159E272CC802A800486F54 /* EventSerializer.swift in Sources */, 522532CA2D295F3F001B5D7C /* AttributeValue.swift in Sources */, 528EAB892C804AA100CB1574 /* SessionManager.swift in Sources */, @@ -1459,6 +1466,7 @@ 52AE72032CABAE9000F2830A /* GestureEvents.swift in Sources */, 52A3C0782CDB732F00C8F047 /* CpuUsageData.swift in Sources */, 5202BE7D2C8B117900A3496E /* EventType.swift in Sources */, + 52BFED302D2E476E00AC1A06 /* UserTriggeredEventCollector.swift in Sources */, 52A8533C2C994BCE00B2A39F /* StackFrame.swift in Sources */, 5225D0332D088B7100FD240D /* URLSessionTaskSwizzler.swift in Sources */, 52CD911E2C7B397C000189BA /* BaseConfigProvider.swift in Sources */, diff --git a/ios/MeasureSDK/Config/Config.swift b/ios/MeasureSDK/Config/Config.swift index 621ccec86..86117f6f3 100644 --- a/ios/MeasureSDK/Config/Config.swift +++ b/ios/MeasureSDK/Config/Config.swift @@ -42,7 +42,7 @@ struct Config: InternalConfig, MeasureConfig { self.enableLogging = enableLogging self.trackScreenshotOnCrash = trackScreenshotOnCrash self.sessionSamplingRate = sessionSamplingRate - self.eventsBatchingIntervalMs = 30000 // 30 seconds + self.eventsBatchingIntervalMs = 3000 // 30 seconds self.maxEventsInBatch = 500 self.sessionEndLastEventThresholdMs = 20 * 60 * 1000 // 20 minitues self.timeoutIntervalForRequest = 30 // 30 seconds diff --git a/ios/MeasureSDK/CoreData/Entities/EventEntity.swift b/ios/MeasureSDK/CoreData/Entities/EventEntity.swift index 3a3211ca5..bd232bf63 100644 --- a/ios/MeasureSDK/CoreData/Entities/EventEntity.swift +++ b/ios/MeasureSDK/CoreData/Entities/EventEntity.swift @@ -28,6 +28,7 @@ struct EventEntity { // swiftlint:disable:this type_body_length let warmLaunch: Data? let hotLaunch: Data? let networkChange: Data? + let screenView: Data? let userTriggered: Bool let attachmentSize: Number let timestampInMillis: Number @@ -211,6 +212,17 @@ struct EventEntity { // swiftlint:disable:this type_body_length self.customEvent = nil } + if let screenView = event.data as? ScreenViewData { + do { + let data = try JSONEncoder().encode(screenView) + self.screenView = data + } catch { + self.screenView = nil + } + } else { + self.screenView = nil + } + if let attributes = event.attributes { do { let data = try JSONEncoder().encode(attributes) @@ -255,7 +267,8 @@ struct EventEntity { // swiftlint:disable:this type_body_length hotLaunch: Data?, http: Data?, networkChange: Data?, - customEvent: Data?) { + customEvent: Data?, + screenView: Data?) { self.id = id self.sessionId = sessionId self.timestamp = timestamp @@ -282,6 +295,7 @@ struct EventEntity { // swiftlint:disable:this type_body_length self.http = http self.networkChange = networkChange self.customEvent = customEvent + self.screenView = screenView } func getEvent() -> Event { // swiftlint:disable:this cyclomatic_complexity function_body_length @@ -408,6 +422,14 @@ struct EventEntity { // swiftlint:disable:this type_body_length decodedData = nil } } + case .screenView: + if let screenViewData = self.screenView { + do { + decodedData = try JSONDecoder().decode(T.self, from: screenViewData) + } catch { + decodedData = nil + } + } case nil: decodedData = nil } diff --git a/ios/MeasureSDK/CoreData/EventStore.swift b/ios/MeasureSDK/CoreData/EventStore.swift index ba355f82f..e4419ccb0 100644 --- a/ios/MeasureSDK/CoreData/EventStore.swift +++ b/ios/MeasureSDK/CoreData/EventStore.swift @@ -55,6 +55,7 @@ final class BaseEventStore: EventStore { eventOb.http = event.http eventOb.networkChange = event.networkChange eventOb.customEvent = event.customEvent + eventOb.screenView = event.screenView do { try context.saveIfNeeded() @@ -101,7 +102,8 @@ final class BaseEventStore: EventStore { hotLaunch: eventOb.hotLaunch, http: eventOb.http, networkChange: eventOb.networkChange, - customEvent: eventOb.customEvent) + customEvent: eventOb.customEvent, + screenView: eventOb.screenView) } } catch { guard let self = self else { return } @@ -146,7 +148,8 @@ final class BaseEventStore: EventStore { hotLaunch: eventOb.hotLaunch, http: eventOb.http, networkChange: eventOb.networkChange, - customEvent: eventOb.customEvent) + customEvent: eventOb.customEvent, + screenView: eventOb.screenView) } } catch { guard let self = self else { return } @@ -210,7 +213,8 @@ final class BaseEventStore: EventStore { hotLaunch: eventOb.hotLaunch, http: eventOb.http, networkChange: eventOb.networkChange, - customEvent: eventOb.customEvent)) + customEvent: eventOb.customEvent, + screenView: eventOb.screenView)) } } catch { guard let self = self else { diff --git a/ios/MeasureSDK/Events/EventType.swift b/ios/MeasureSDK/Events/EventType.swift index 664effde2..871bbae44 100644 --- a/ios/MeasureSDK/Events/EventType.swift +++ b/ios/MeasureSDK/Events/EventType.swift @@ -23,4 +23,5 @@ enum EventType: String, Codable { case http case networkChange = "network_change" case custom + case screenView = "screen_view" } diff --git a/ios/MeasureSDK/Events/ScreenViewData.swift b/ios/MeasureSDK/Events/ScreenViewData.swift new file mode 100644 index 000000000..d66cf1636 --- /dev/null +++ b/ios/MeasureSDK/Events/ScreenViewData.swift @@ -0,0 +1,12 @@ +// +// ScreenViewData.swift +// MeasureSDK +// +// Created by Adwin Ross on 08/01/25. +// + +import Foundation + +struct ScreenViewData: Codable { + let name: String +} diff --git a/ios/MeasureSDK/Events/UserTriggeredEventCollector.swift b/ios/MeasureSDK/Events/UserTriggeredEventCollector.swift new file mode 100644 index 000000000..3504d5e10 --- /dev/null +++ b/ios/MeasureSDK/Events/UserTriggeredEventCollector.swift @@ -0,0 +1,46 @@ +// +// UserTriggeredEventCollector.swift +// MeasureSDK +// +// Created by Adwin Ross on 08/01/25. +// + +import Foundation + +protocol UserTriggeredEventCollector { + func trackScreenView(_ screenName: String) + func enable() + func disable() +} + +final class BaseUserTriggeredEventCollector: UserTriggeredEventCollector { + private let eventProcessor: EventProcessor + private let timeProvider: TimeProvider + private var isEnabled = false + + init(eventProcessor: EventProcessor, timeProvider: TimeProvider) { + self.eventProcessor = eventProcessor + self.timeProvider = timeProvider + } + + func enable() { + isEnabled = true + } + + func disable() { + isEnabled = false + } + + func trackScreenView(_ screenName: String) { + guard isEnabled else { return } + + let data = ScreenViewData(name: screenName) + eventProcessor.trackUserTriggered(data: data, + timestamp: timeProvider.now(), + type: .screenView, + attributes: nil, + sessionId: nil, + attachments: nil, + userDefinedAttributes: nil) + } +} diff --git a/ios/MeasureSDK/Exporter/EventSerializer.swift b/ios/MeasureSDK/Exporter/EventSerializer.swift index 4eb2b567e..5c3a50b10 100644 --- a/ios/MeasureSDK/Exporter/EventSerializer.swift +++ b/ios/MeasureSDK/Exporter/EventSerializer.swift @@ -161,6 +161,16 @@ struct EventSerializer { // swiftlint:disable:this type_body_length } } return nil + case .screenView: + if let screenViewData = event.screenView { + do { + let decodedData = try JSONDecoder().decode(ScreenViewData.self, from: screenViewData) + return serialiseScreenViewData(decodedData) + } catch { + return nil + } + } + return nil case nil: return nil } @@ -418,6 +428,13 @@ struct EventSerializer { // swiftlint:disable:this type_body_length return result } + private func serialiseScreenViewData(_ screenViewData: ScreenViewData) -> String { + var result = "{" + result += "\"name\":\"\(screenViewData.name)\"" + result += "}" + return result + } + private func getSerialisedAttributes(for event: EventEntity) -> String? { let decodedAttributes: Attributes? if let attributeData = event.attributes { diff --git a/ios/MeasureSDK/Measure.swift b/ios/MeasureSDK/Measure.swift index 6ea1ad5d3..a1b98e8fa 100644 --- a/ios/MeasureSDK/Measure.swift +++ b/ios/MeasureSDK/Measure.swift @@ -159,4 +159,24 @@ import Foundation customEventCollector.trackEvent(name: name, attributes: transformedAttributes, timestamp: timestamp?.int64Value) } + + /// Call when a screen is viewed by the user. + /// + /// Measure SDK automatically collects screen view events for UIKit and SwiftUI navigation. + /// However, if your app uses a custom navigation system, you can use this method to track + /// screen view events and gain more context when debugging issues. + /// + /// Example usage: + /// ```swift + /// Measure.shared.trackScreenView("Home") + /// ``` + /// + /// ```objc + /// [[Measure shared] trackScreenView:@"ObjcViewController"] + /// ``` + @objc public func trackScreenView(_ screenName: String) { + guard let userTriggeredEventCollector = measureInternal?.userTriggeredEventCollector else { return } + + userTriggeredEventCollector.trackScreenView(screenName) + } } diff --git a/ios/MeasureSDK/MeasureInitializer.swift b/ios/MeasureSDK/MeasureInitializer.swift index 8471ef5ab..4247d086d 100644 --- a/ios/MeasureSDK/MeasureInitializer.swift +++ b/ios/MeasureSDK/MeasureInitializer.swift @@ -50,6 +50,7 @@ protocol MeasureInitializer { var httpEventCollector: HttpEventCollector { get } var networkChangeCollector: NetworkChangeCollector { get } var customEventCollector: CustomEventCollector { get } + var userTriggeredEventCollector: UserTriggeredEventCollector { get } } /// `BaseMeasureInitializer` is responsible for setting up the internal configuration @@ -82,6 +83,7 @@ protocol MeasureInitializer { /// - `memoryUsageCollector`: `MemoryUsageCollector` object which is responsible for detecting and saving memory usage data. /// - `appLaunchCollector`: `AppLaunchCollector` object which is responsible for detecting and saving launch related events. /// - `httpEventCollector`: `HttpEventCollector` object that collects HTTP request data. +/// - `userTriggeredEventCollector`: `UserTriggeredEventCollector` object which is responsible for tracking user triggered events. /// - `gestureTargetFinder`: `GestureTargetFinder` object that determines which view is handling the gesture. /// - `cpuUsageCalculator`: `CpuUsageCalculator` object that generates CPU usage data. /// - `memoryUsageCalculator`: `MemoryUsageCalculator` object that generates memory usage data. @@ -136,6 +138,7 @@ final class BaseMeasureInitializer: MeasureInitializer { var httpEventCollector: HttpEventCollector let networkChangeCollector: NetworkChangeCollector let customEventCollector: CustomEventCollector + let userTriggeredEventCollector: UserTriggeredEventCollector init(config: MeasureConfig, // swiftlint:disable:this function_body_length client: Client) { @@ -251,6 +254,8 @@ final class BaseMeasureInitializer: MeasureInitializer { eventProcessor: eventProcessor, timeProvider: timeProvider, configProvider: configProvider) + self.userTriggeredEventCollector = BaseUserTriggeredEventCollector(eventProcessor: eventProcessor, + timeProvider: timeProvider) self.client = client self.httpEventCollector = BaseHttpEventCollector(logger: logger, eventProcessor: eventProcessor, diff --git a/ios/MeasureSDK/MeasureInternal.swift b/ios/MeasureSDK/MeasureInternal.swift index 8773f4a5b..c801a4d81 100644 --- a/ios/MeasureSDK/MeasureInternal.swift +++ b/ios/MeasureSDK/MeasureInternal.swift @@ -112,6 +112,9 @@ final class MeasureInternal { var customEventCollector: CustomEventCollector { return measureInitializer.customEventCollector } + var userTriggeredEventCollector: UserTriggeredEventCollector { + return measureInitializer.userTriggeredEventCollector + } private let lifecycleObserver: LifecycleObserver init(_ measureInitializer: MeasureInitializer) { @@ -122,6 +125,7 @@ final class MeasureInternal { self.sessionManager.start() self.customEventCollector.enable() self.appLaunchCollector.enable() + self.userTriggeredEventCollector.enable() self.lifecycleObserver.applicationDidEnterBackground = applicationDidEnterBackground self.lifecycleObserver.applicationWillEnterForeground = applicationWillEnterForeground self.lifecycleObserver.applicationWillTerminate = applicationWillTerminate diff --git a/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents b/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents index db8ba1580..6af05e6ed 100644 --- a/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents +++ b/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents @@ -25,6 +25,7 @@ +