diff --git a/ios/DemoApp/AppDelegate.swift b/ios/DemoApp/AppDelegate.swift index 55f01b876..a5091d9b6 100644 --- a/ios/DemoApp/AppDelegate.swift +++ b/ios/DemoApp/AppDelegate.swift @@ -18,7 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { apiUrl: "http://localhost:8080") let config = BaseMeasureConfig(enableLogging: true, trackScreenshotOnCrash: false, - sessionSamplingRate: 1.0) + samplingRateForErrorFreeSessions: 0.5) measureInstance.initialize(with: clientInfo, config: config) return true diff --git a/ios/MeasureSDK.xcodeproj/project.pbxproj b/ios/MeasureSDK.xcodeproj/project.pbxproj index ed28dc5c2..234319d12 100644 --- a/ios/MeasureSDK.xcodeproj/project.pbxproj +++ b/ios/MeasureSDK.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 5222C9E82D14605000B198DA /* NetworkInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222C9E72D14605000B198DA /* NetworkInterceptor.swift */; }; 5224ECE02C88057A00D1B1F7 /* FatalErrorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5224ECDF2C88057A00D1B1F7 /* FatalErrorUtil.swift */; }; 5224ECE32C880FA400D1B1F7 /* XCTextCase+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5224ECE22C880FA300D1B1F7 /* XCTextCase+Extension.swift */; }; + 522532CA2D295F3F001B5D7C /* AttributeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522532C92D295F3F001B5D7C /* AttributeValue.swift */; }; 5225D02E2D088B7100FD240D /* HttpData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0272D088B7100FD240D /* HttpData.swift */; }; 5225D02F2D088B7100FD240D /* HttpEventCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0282D088B7100FD240D /* HttpEventCollector.swift */; }; 5225D0302D088B7100FD240D /* HttpInterceptorCallbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */; }; @@ -47,8 +48,9 @@ 5225D0332D088B7100FD240D /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D02C2D088B7100FD240D /* URLSessionTaskSwizzler.swift */; }; 5225D0352D0AEB1A00FD240D /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0342D0AEB1A00FD240D /* String+Extension.swift */; }; 5225D0502D0FECFF00FD240D /* InputStream+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */; }; - 522532CA2D295F3F001B5D7C /* AttributeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522532C92D295F3F001B5D7C /* AttributeValue.swift */; }; 5229D16E2CCB533C00EFFE44 /* RecentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5229D16D2CCB533C00EFFE44 /* RecentSession.swift */; }; + 522BA9D42D36579100DBF4A3 /* DataCleanupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */; }; + 522BA9D62D37C2A000DBF4A3 /* DataCleanupServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */; }; 523287692C85E07B000EE268 /* LifecycleObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287682C85E07B000EE268 /* LifecycleObserverTests.swift */; }; 523287732C86195E000EE268 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287722C86195E000EE268 /* SessionManagerTests.swift */; }; 523287752C8619C4000EE268 /* MockIdProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287742C8619C4000EE268 /* MockIdProvider.swift */; }; @@ -359,6 +361,7 @@ 5222C9E72D14605000B198DA /* NetworkInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInterceptor.swift; sourceTree = ""; }; 5224ECDF2C88057A00D1B1F7 /* FatalErrorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorUtil.swift; sourceTree = ""; }; 5224ECE22C880FA300D1B1F7 /* XCTextCase+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTextCase+Extension.swift"; sourceTree = ""; }; + 522532C92D295F3F001B5D7C /* AttributeValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeValue.swift; sourceTree = ""; }; 5225D0272D088B7100FD240D /* HttpData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpData.swift; sourceTree = ""; }; 5225D0282D088B7100FD240D /* HttpEventCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpEventCollector.swift; sourceTree = ""; }; 5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpInterceptorCallbacks.swift; sourceTree = ""; }; @@ -367,8 +370,9 @@ 5225D02C2D088B7100FD240D /* URLSessionTaskSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzler.swift; sourceTree = ""; }; 5225D0342D0AEB1A00FD240D /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputStream+Extension.swift"; sourceTree = ""; }; - 522532C92D295F3F001B5D7C /* AttributeValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeValue.swift; sourceTree = ""; }; 5229D16D2CCB533C00EFFE44 /* RecentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSession.swift; sourceTree = ""; }; + 522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupService.swift; sourceTree = ""; }; + 522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupServiceTests.swift; sourceTree = ""; }; 523287682C85E07B000EE268 /* LifecycleObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleObserverTests.swift; sourceTree = ""; }; 523287722C86195E000EE268 /* SessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = ""; }; 523287742C8619C4000EE268 /* MockIdProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdProvider.swift; sourceTree = ""; }; @@ -803,6 +807,7 @@ children = ( 524576722CC116DD00B288E5 /* BatchStore.swift */, 524CC5D22C6A4B48001AB506 /* CoreDataManager.swift */, + 522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */, 52A1A94B2CA3D3B000461103 /* Entities */, 52A1A9492CA3CF9B00461103 /* EventStore.swift */, 52A1A93B2CA0777300461103 /* SessionStore.swift */, @@ -894,6 +899,7 @@ isa = PBXGroup; children = ( 52D3D7372CC404DA004E404B /* BatchStoreTests.swift */, + 522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */, 52A1A94E2CA52C8A00461103 /* EventStoreTests.swift */, 52A1A9402CA087BA00461103 /* SessionStoreTests.swift */, ); @@ -1401,6 +1407,7 @@ 5202BE472C89600200A3496E /* UIDevice+Extension.swift in Sources */, 52AE72012CABAE9000F2830A /* GestureCollector.swift in Sources */, 5202BE3B2C895FC800A3496E /* AttributeProcessor.swift in Sources */, + 522BA9D42D36579100DBF4A3 /* DataCleanupService.swift in Sources */, 5202BE3C2C895FC800A3496E /* ComputeOnceAttributeProcessor.swift in Sources */, 5202BE3D2C895FC800A3496E /* DeviceAttributeProcessor.swift in Sources */, 52BCF1DC2CB42026003102DF /* MeasureModel.xcdatamodeld in Sources */, @@ -1523,6 +1530,7 @@ 5202BE582C89603400A3496E /* MockUserDefaultStorage.swift in Sources */, 52D3D73F2CC4E696004E404B /* EventExporterTests.swift in Sources */, 52D3D73D2CC415B2004E404B /* MockBatchStore.swift in Sources */, + 522BA9D62D37C2A000DBF4A3 /* DataCleanupServiceTests.swift in Sources */, 5202BE522C89601200A3496E /* UserAttributeProcessorTests.swift in Sources */, 5224ECE32C880FA400D1B1F7 /* XCTextCase+Extension.swift in Sources */, 52A1A9662CA5AC9900461103 /* Attachment+Extension.swift in Sources */, diff --git a/ios/MeasureSDK/Config/BaseConfigProvider.swift b/ios/MeasureSDK/Config/BaseConfigProvider.swift index d63723207..a07333869 100644 --- a/ios/MeasureSDK/Config/BaseConfigProvider.swift +++ b/ios/MeasureSDK/Config/BaseConfigProvider.swift @@ -33,6 +33,10 @@ final class BaseConfigProvider: ConfigProvider { self.cachedConfig = configLoader.getCachedConfig() } + var eventTypeExportAllowList: [EventType] { + return getMergedConfig(\.eventTypeExportAllowList) + } + var maxUserDefinedAttributesPerEvent: Int { return getMergedConfig(\.maxUserDefinedAttributesPerEvent) } @@ -69,8 +73,8 @@ final class BaseConfigProvider: ConfigProvider { return getMergedConfig(\.maxEventsInBatch) } - var sessionSamplingRate: Float { - return getMergedConfig(\.sessionSamplingRate) + var samplingRateForErrorFreeSessions: Float { + return getMergedConfig(\.samplingRateForErrorFreeSessions) } var enableLogging: Bool { diff --git a/ios/MeasureSDK/Config/Config.swift b/ios/MeasureSDK/Config/Config.swift index 86117f6f3..036d75c68 100644 --- a/ios/MeasureSDK/Config/Config.swift +++ b/ios/MeasureSDK/Config/Config.swift @@ -17,7 +17,7 @@ import Foundation struct Config: InternalConfig, MeasureConfig { let enableLogging: Bool let trackScreenshotOnCrash: Bool - let sessionSamplingRate: Float + let samplingRateForErrorFreeSessions: Float let eventsBatchingIntervalMs: Number let sessionEndLastEventThresholdMs: Number let longPressTimeout: TimeInterval @@ -35,14 +35,15 @@ struct Config: InternalConfig, MeasureConfig { let maxUserDefinedAttributeKeyLength: Int let maxUserDefinedAttributeValueLength: Int let maxUserDefinedAttributesPerEvent: Int + let eventTypeExportAllowList: [EventType] internal init(enableLogging: Bool = DefaultConfig.enableLogging, trackScreenshotOnCrash: Bool = DefaultConfig.trackScreenshotOnCrash, - sessionSamplingRate: Float = DefaultConfig.sessionSamplingRate) { + samplingRateForErrorFreeSessions: Float = DefaultConfig.sessionSamplingRate) { self.enableLogging = enableLogging self.trackScreenshotOnCrash = trackScreenshotOnCrash - self.sessionSamplingRate = sessionSamplingRate - self.eventsBatchingIntervalMs = 3000 // 30 seconds + self.samplingRateForErrorFreeSessions = samplingRateForErrorFreeSessions + self.eventsBatchingIntervalMs = 30000 // 30 seconds self.maxEventsInBatch = 500 self.sessionEndLastEventThresholdMs = 20 * 60 * 1000 // 20 minitues self.timeoutIntervalForRequest = 30 // 30 seconds @@ -64,5 +65,11 @@ struct Config: InternalConfig, MeasureConfig { self.maxUserDefinedAttributeKeyLength = 256 // 256 chars self.maxUserDefinedAttributeValueLength = 256 // 256 chars self.maxUserDefinedAttributesPerEvent = 100 + self.eventTypeExportAllowList = [.coldLaunch, + .hotLaunch, + .warmLaunch, + .lifecycleSwiftUI, + .lifecycleViewController, + .screenView] } } diff --git a/ios/MeasureSDK/Config/DefaultConfig.swift b/ios/MeasureSDK/Config/DefaultConfig.swift index f3b79d8e0..187fe44dc 100644 --- a/ios/MeasureSDK/Config/DefaultConfig.swift +++ b/ios/MeasureSDK/Config/DefaultConfig.swift @@ -11,5 +11,5 @@ import Foundation struct DefaultConfig { static let enableLogging = false static let trackScreenshotOnCrash = true - static let sessionSamplingRate: Float = 1.0 + static let sessionSamplingRate: Float = 0.0 } diff --git a/ios/MeasureSDK/Config/InternalConfig.swift b/ios/MeasureSDK/Config/InternalConfig.swift index f32e8276a..38abc68a2 100644 --- a/ios/MeasureSDK/Config/InternalConfig.swift +++ b/ios/MeasureSDK/Config/InternalConfig.swift @@ -59,4 +59,8 @@ protocol InternalConfig { /// The maximum number of user defined attributes for an event. Defaults to 100. var maxUserDefinedAttributesPerEvent: Int { get } + + /// All `EventType`s that are always exported, regardless of other filters like session sampling rate and whether the session crashed or not. + var eventTypeExportAllowList: [EventType] { get } + } diff --git a/ios/MeasureSDK/Config/MeasureConfig.swift b/ios/MeasureSDK/Config/MeasureConfig.swift index 11ceb19d8..44883a21e 100644 --- a/ios/MeasureSDK/Config/MeasureConfig.swift +++ b/ios/MeasureSDK/Config/MeasureConfig.swift @@ -11,7 +11,7 @@ import Foundation protocol MeasureConfig { var enableLogging: Bool { get } var trackScreenshotOnCrash: Bool { get } - var sessionSamplingRate: Float { get } + var samplingRateForErrorFreeSessions: Float { get } } /// Configuration options for the Measure SDK. Used to customize the behavior of the SDK on initialization. @@ -24,21 +24,22 @@ protocol MeasureConfig { @objc public final class BaseMeasureConfig: NSObject, MeasureConfig { let enableLogging: Bool let trackScreenshotOnCrash: Bool - let sessionSamplingRate: Float + let samplingRateForErrorFreeSessions: Float /// Configuration options for the Measure SDK. Used to customize the behavior of the SDK on initialization. /// - Parameters: /// - enableLogging: Enable or disable internal SDK logs. Defaults to `false`. /// - trackScreenshotOnCrash: Whether to capture a screenshot of the app when it crashes due to an unhandled exception. Defaults to `true`. - /// - sessionSamplingRate: Allows setting a sampling rate for non-crashed sessions. Session sampling rate must be between 0.0 and 1.0. By default, all non-crashed sessions are always exported. + /// - samplingRateForErrorFreeSessions: Sampling rate for sessions without a crash. The sampling rate is a value between 0 and 1. + /// For example, a value of `0.5` will export only 50% of the non-crashed sessions, and a value of `0` will disable sending non-crashed sessions to the server. public init(enableLogging: Bool? = nil, trackScreenshotOnCrash: Bool? = nil, - sessionSamplingRate: Float? = nil) { + samplingRateForErrorFreeSessions: Float? = nil) { self.enableLogging = enableLogging ?? DefaultConfig.enableLogging self.trackScreenshotOnCrash = trackScreenshotOnCrash ?? DefaultConfig.trackScreenshotOnCrash - self.sessionSamplingRate = sessionSamplingRate ?? DefaultConfig.sessionSamplingRate + self.samplingRateForErrorFreeSessions = samplingRateForErrorFreeSessions ?? DefaultConfig.sessionSamplingRate - if !(0.0...1.0).contains(self.sessionSamplingRate) { + if !(0.0...1.0).contains(self.samplingRateForErrorFreeSessions) { fatalError("Session sampling rate must be between 0.0 and 1.0") } } diff --git a/ios/MeasureSDK/CoreData/DataCleanupService.swift b/ios/MeasureSDK/CoreData/DataCleanupService.swift new file mode 100644 index 000000000..0dc64036c --- /dev/null +++ b/ios/MeasureSDK/CoreData/DataCleanupService.swift @@ -0,0 +1,46 @@ +// +// DataCleanupService.swift +// MeasureSDK +// +// Created by Adwin Ross on 14/01/25. +// + +import Foundation + +protocol DataCleanupService { + func clearStaleData() +} + +final class BaseDataCleanupService: DataCleanupService { + private let eventStore: EventStore + private let sessionStore: SessionStore + private let logger: Logger + private let sessionManager: SessionManager + + init(eventStore: EventStore, sessionStore: SessionStore, logger: Logger, sessionManager: SessionManager) { + self.eventStore = eventStore + self.sessionStore = sessionStore + self.logger = logger + self.sessionManager = sessionManager + } + + func clearStaleData() { + guard let sessionsToDelete = getSessionsToDelete() else { + logger.internalLog(level: .info, message: "No session data to clear.", error: nil, data: nil) + return + } + + sessionStore.deleteSessions(sessionsToDelete) + eventStore.deleteEvents(sessionIds: sessionsToDelete) + } + + private func getSessionsToDelete() -> [String]? { + guard var sessionsToDelete = sessionStore.getSessionsToDelete() else { + return nil + } + + sessionsToDelete.removeAll { $0 == sessionManager.sessionId } + + return sessionsToDelete + } +} diff --git a/ios/MeasureSDK/CoreData/Entities/EventEntity.swift b/ios/MeasureSDK/CoreData/Entities/EventEntity.swift index bd232bf63..deae7d6c8 100644 --- a/ios/MeasureSDK/CoreData/Entities/EventEntity.swift +++ b/ios/MeasureSDK/CoreData/Entities/EventEntity.swift @@ -35,8 +35,9 @@ struct EventEntity { // swiftlint:disable:this type_body_length var batchId: String? let http: Data? let customEvent: Data? + var needsReporting: Bool - init(_ event: Event) { // swiftlint:disable:this cyclomatic_complexity function_body_length + init(_ event: Event, needsReporting: Bool) { // swiftlint:disable:this cyclomatic_complexity function_body_length self.id = event.id self.sessionId = event.sessionId self.timestamp = event.timestamp @@ -46,6 +47,7 @@ struct EventEntity { // swiftlint:disable:this type_body_length self.attachmentSize = 0 self.batchId = nil self.userDefinedAttributes = event.userDefinedAttributes + self.needsReporting = needsReporting if let exception = event.data as? Exception { do { @@ -268,7 +270,8 @@ struct EventEntity { // swiftlint:disable:this type_body_length http: Data?, networkChange: Data?, customEvent: Data?, - screenView: Data?) { + screenView: Data?, + needsReporting: Bool) { self.id = id self.sessionId = sessionId self.timestamp = timestamp @@ -296,6 +299,7 @@ struct EventEntity { // swiftlint:disable:this type_body_length self.networkChange = networkChange self.customEvent = customEvent self.screenView = screenView + self.needsReporting = needsReporting } func getEvent() -> Event { // swiftlint:disable:this cyclomatic_complexity function_body_length diff --git a/ios/MeasureSDK/CoreData/EventStore.swift b/ios/MeasureSDK/CoreData/EventStore.swift index e4419ccb0..792e456bb 100644 --- a/ios/MeasureSDK/CoreData/EventStore.swift +++ b/ios/MeasureSDK/CoreData/EventStore.swift @@ -13,9 +13,11 @@ protocol EventStore { func getEvents(eventIds: [String]) -> [EventEntity]? func getEventsForSessions(sessions: [String]) -> [EventEntity]? func deleteEvents(eventIds: [String]) + func deleteEvents(sessionIds: [String]) func getAllEvents() -> [EventEntity]? func getUnBatchedEventsWithAttachmentSize(eventCount: Number, ascending: Bool, sessionId: String?) -> [String: Number] func updateBatchId(_ batchId: String, for events: [String]) + func updateNeedsReportingForAllEvents(sessionId: String, needsReporting: Bool) } final class BaseEventStore: EventStore { @@ -56,6 +58,8 @@ final class BaseEventStore: EventStore { eventOb.networkChange = event.networkChange eventOb.customEvent = event.customEvent eventOb.screenView = event.screenView + eventOb.timestampInMillis = event.timestampInMillis + eventOb.needsReporting = event.needsReporting do { try context.saveIfNeeded() @@ -103,7 +107,8 @@ final class BaseEventStore: EventStore { http: eventOb.http, networkChange: eventOb.networkChange, customEvent: eventOb.customEvent, - screenView: eventOb.screenView) + screenView: eventOb.screenView, + needsReporting: eventOb.needsReporting) } } catch { guard let self = self else { return } @@ -149,7 +154,8 @@ final class BaseEventStore: EventStore { http: eventOb.http, networkChange: eventOb.networkChange, customEvent: eventOb.customEvent, - screenView: eventOb.screenView) + screenView: eventOb.screenView, + needsReporting: eventOb.needsReporting) } } catch { guard let self = self else { return } @@ -214,7 +220,8 @@ final class BaseEventStore: EventStore { http: eventOb.http, networkChange: eventOb.networkChange, customEvent: eventOb.customEvent, - screenView: eventOb.screenView)) + screenView: eventOb.screenView, + needsReporting: eventOb.needsReporting)) } } catch { guard let self = self else { @@ -238,13 +245,14 @@ final class BaseEventStore: EventStore { var predicates = [NSPredicate]() predicates.append(NSPredicate(format: "batchId == nil")) + if let sessionId = sessionId { predicates.append(NSPredicate(format: "sessionId == %@", sessionId)) } - if !predicates.isEmpty { - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) - } + predicates.append(NSPredicate(format: "needsReporting == %d", true)) + + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) var eventIdAttachmentSizeMap: [String: Int64] = [:] @@ -285,4 +293,42 @@ final class BaseEventStore: EventStore { } } } + + func updateNeedsReportingForAllEvents(sessionId: String, needsReporting: Bool) { + let context = coreDataManager.backgroundContext + let fetchRequest: NSFetchRequest = EventOb.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "sessionId == %@", sessionId) + + context.performAndWait { [weak self] in + do { + let events = try context.fetch(fetchRequest) + for event in events { + event.needsReporting = needsReporting + } + try context.saveIfNeeded() + } catch { + guard let self = self else { return } + self.logger.internalLog(level: .error, message: "Failed to update needsReporting for sessionId: \(sessionId)", error: error, data: nil) + } + } + } + + func deleteEvents(sessionIds: [String]) { + let context = coreDataManager.backgroundContext + let fetchRequest: NSFetchRequest = EventOb.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "sessionId IN %@ AND needsReporting == %d", sessionIds, false) + + context.performAndWait { [weak self] in + do { + let events = try context.fetch(fetchRequest) + for event in events { + context.delete(event) + } + try context.saveIfNeeded() + } catch { + guard let self = self else { return } + self.logger.internalLog(level: .error, message: "Failed to delete events by session IDs: \(sessionIds.joined(separator: ","))", error: error, data: nil) + } + } + } } diff --git a/ios/MeasureSDK/CoreData/SessionStore.swift b/ios/MeasureSDK/CoreData/SessionStore.swift index 16a836452..0a59740ca 100644 --- a/ios/MeasureSDK/CoreData/SessionStore.swift +++ b/ios/MeasureSDK/CoreData/SessionStore.swift @@ -15,8 +15,10 @@ protocol SessionStore { func deleteSession(_ sessionId: String) func markCrashedSessions(sessionIds: [String]) func markCrashedSession(sessionId: String) + func updateNeedsReporting(sessionId: String, needsReporting: Bool) func getOldestSession() -> String? func deleteSessions(_ sessionIds: [String]) + func getSessionsToDelete() -> [String]? } final class BaseSessionStore: SessionStore { @@ -30,7 +32,7 @@ final class BaseSessionStore: SessionStore { func insertSession(_ session: SessionEntity) { let context = coreDataManager.backgroundContext - context.perform { [weak self] in + context.performAndWait { [weak self] in let sessionOb = SessionOb(context: context) sessionOb.sessionId = session.sessionId @@ -77,7 +79,7 @@ final class BaseSessionStore: SessionStore { let context = coreDataManager.backgroundContext let fetchRequest: NSFetchRequest = SessionOb.fetchRequest() - var sessions: [SessionEntity]? + var sessions = [SessionEntity]() context.performAndWait { [weak self] in do { let result = try context.fetch(fetchRequest) @@ -93,7 +95,7 @@ final class BaseSessionStore: SessionStore { self.logger.internalLog(level: .error, message: "Failed to fetch sessions.", error: error, data: nil) } } - return sessions + return sessions.isEmpty ? nil : sessions } func deleteSession(_ sessionId: String) { @@ -102,7 +104,7 @@ final class BaseSessionStore: SessionStore { fetchRequest.fetchLimit = 1 fetchRequest.predicate = NSPredicate(format: "sessionId == %@", sessionId) - context.perform { [weak self] in + context.performAndWait { [weak self] in do { if let sessionOb = try context.fetch(fetchRequest).first { context.delete(sessionOb) @@ -121,7 +123,7 @@ final class BaseSessionStore: SessionStore { fetchRequest.fetchLimit = sessionIds.count fetchRequest.predicate = NSPredicate(format: "sessionId IN %@", sessionIds) - context.perform { [weak self] in + context.performAndWait { [weak self] in do { let sessions = try context.fetch(fetchRequest) for session in sessions { @@ -141,7 +143,7 @@ final class BaseSessionStore: SessionStore { fetchRequest.fetchLimit = 1 fetchRequest.predicate = NSPredicate(format: "sessionId == %@", sessionId) - context.perform { [weak self] in + context.performAndWait { [weak self] in do { if let session = try context.fetch(fetchRequest).first { session.crashed = true @@ -154,6 +156,25 @@ final class BaseSessionStore: SessionStore { } } + func updateNeedsReporting(sessionId: String, needsReporting: Bool) { + let context = coreDataManager.backgroundContext + let fetchRequest: NSFetchRequest = SessionOb.fetchRequest() + fetchRequest.fetchLimit = 1 + fetchRequest.predicate = NSPredicate(format: "sessionId == %@", sessionId) + + context.performAndWait { [weak self] in + do { + if let session = try context.fetch(fetchRequest).first { + session.needsReporting = needsReporting + try context.saveIfNeeded() + } + } catch { + guard let self = self else { return } + self.logger.internalLog(level: .error, message: "Failed to mark crashed session: \(sessionId)", error: error, data: nil) + } + } + } + func getOldestSession() -> String? { let context = coreDataManager.backgroundContext let fetchRequest: NSFetchRequest = SessionOb.fetchRequest() @@ -179,7 +200,7 @@ final class BaseSessionStore: SessionStore { fetchRequest.fetchLimit = sessionIds.count fetchRequest.predicate = NSPredicate(format: "sessionId IN %@", sessionIds) - context.perform { [weak self] in + context.performAndWait { [weak self] in do { let sessions = try context.fetch(fetchRequest) for session in sessions { @@ -192,4 +213,23 @@ final class BaseSessionStore: SessionStore { } } } + + func getSessionsToDelete() -> [String]? { + let context = coreDataManager.backgroundContext + let fetchRequest: NSFetchRequest = SessionOb.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "needsReporting == %d", false) + + var sessionIds: [String]? + context.performAndWait { [weak self] in + do { + let sessions = try context.fetch(fetchRequest) + sessionIds = sessions.compactMap { $0.sessionId } + } catch { + guard let self = self else { return } + self.logger.internalLog(level: .error, message: "Failed to fetch sessions to delete.", error: error, data: nil) + } + } + + return sessionIds + } } diff --git a/ios/MeasureSDK/Events/EventProcessor.swift b/ios/MeasureSDK/Events/EventProcessor.swift index 7e7404011..18d137954 100644 --- a/ios/MeasureSDK/Events/EventProcessor.swift +++ b/ios/MeasureSDK/Events/EventProcessor.swift @@ -130,7 +130,8 @@ final class BaseEventProcessor: EventProcessor { self.crashDataPersistence.attribute = attributes } - let eventEntity = EventEntity(event) + let needsReporting = sessionManager.shouldReportSession ? true : configProvider.eventTypeExportAllowList.contains(event.type) + let eventEntity = EventEntity(event, needsReporting: needsReporting) eventStore.insertEvent(event: eventEntity) sessionManager.onEventTracked(eventEntity) logger.log(level: .debug, message: "Event processed: \(type), \(event.id)", error: nil, data: data) diff --git a/ios/MeasureSDK/Exporter/EventSerializer.swift b/ios/MeasureSDK/Exporter/EventSerializer.swift index 5c3a50b10..96f762fdf 100644 --- a/ios/MeasureSDK/Exporter/EventSerializer.swift +++ b/ios/MeasureSDK/Exporter/EventSerializer.swift @@ -508,4 +508,4 @@ struct EventSerializer { // swiftlint:disable:this type_body_length result += "}" return result } -} +} // swiftlint:disable:this file_length diff --git a/ios/MeasureSDK/MeasureInitializer.swift b/ios/MeasureSDK/MeasureInitializer.swift index 4247d086d..c22c1ba48 100644 --- a/ios/MeasureSDK/MeasureInitializer.swift +++ b/ios/MeasureSDK/MeasureInitializer.swift @@ -51,6 +51,7 @@ protocol MeasureInitializer { var networkChangeCollector: NetworkChangeCollector { get } var customEventCollector: CustomEventCollector { get } var userTriggeredEventCollector: UserTriggeredEventCollector { get } + var dataCleanupService: DataCleanupService { get } } /// `BaseMeasureInitializer` is responsible for setting up the internal configuration @@ -96,6 +97,7 @@ protocol MeasureInitializer { /// - `eventExporter`: `EventExporter` object that exports a single batch. /// - `batchStore`: `BatchStore` object that manages `Batch` related operations /// - `batchCreator`: `BatchCreator` object used to create a batch. +/// - `dataCleanupService`: `DataCleanupService` object responsible for clearing stale data /// final class BaseMeasureInitializer: MeasureInitializer { let configProvider: ConfigProvider @@ -139,12 +141,13 @@ final class BaseMeasureInitializer: MeasureInitializer { let networkChangeCollector: NetworkChangeCollector let customEventCollector: CustomEventCollector let userTriggeredEventCollector: UserTriggeredEventCollector + let dataCleanupService: DataCleanupService init(config: MeasureConfig, // swiftlint:disable:this function_body_length client: Client) { let defaultConfig = Config(enableLogging: config.enableLogging, trackScreenshotOnCrash: config.trackScreenshotOnCrash, - sessionSamplingRate: config.sessionSamplingRate) + samplingRateForErrorFreeSessions: config.samplingRateForErrorFreeSessions) self.configProvider = BaseConfigProvider(defaultConfig: defaultConfig, configLoader: BaseConfigLoader()) @@ -162,6 +165,7 @@ final class BaseMeasureInitializer: MeasureInitializer { timeProvider: timeProvider, configProvider: configProvider, sessionStore: sessionStore, + eventStore: eventStore, userDefaultStorage: userDefaultStorage, versionCode: FrameworkInfo.version) self.appAttributeProcessor = AppAttributeProcessor() @@ -256,6 +260,10 @@ final class BaseMeasureInitializer: MeasureInitializer { configProvider: configProvider) self.userTriggeredEventCollector = BaseUserTriggeredEventCollector(eventProcessor: eventProcessor, timeProvider: timeProvider) + self.dataCleanupService = BaseDataCleanupService(eventStore: eventStore, + sessionStore: sessionStore, + logger: logger, + sessionManager: sessionManager) self.client = client self.httpEventCollector = BaseHttpEventCollector(logger: logger, eventProcessor: eventProcessor, diff --git a/ios/MeasureSDK/MeasureInternal.swift b/ios/MeasureSDK/MeasureInternal.swift index c801a4d81..c3b8324e2 100644 --- a/ios/MeasureSDK/MeasureInternal.swift +++ b/ios/MeasureSDK/MeasureInternal.swift @@ -115,6 +115,9 @@ final class MeasureInternal { var userTriggeredEventCollector: UserTriggeredEventCollector { return measureInitializer.userTriggeredEventCollector } + var dataCleanupService: DataCleanupService { + return measureInitializer.dataCleanupService + } private let lifecycleObserver: LifecycleObserver init(_ measureInitializer: MeasureInitializer) { @@ -154,6 +157,7 @@ final class MeasureInternal { self.lifecycleCollector.applicationDidEnterBackground() self.cpuUsageCollector.pause() self.memoryUsageCollector.pause() + self.dataCleanupService.clearStaleData() } private func applicationWillEnterForeground() { diff --git a/ios/MeasureSDK/SessionManager.swift b/ios/MeasureSDK/SessionManager.swift index f49b590fc..b608f71a9 100644 --- a/ios/MeasureSDK/SessionManager.swift +++ b/ios/MeasureSDK/SessionManager.swift @@ -10,6 +10,7 @@ import Foundation /// Protocol defining the requirements for initializing the SessionManager. protocol SessionManager { var sessionId: String { get } + var shouldReportSession: Bool { get } func start() func applicationDidEnterBackground() func applicationWillEnterForeground() @@ -31,9 +32,11 @@ final class BaseSessionManager: SessionManager { private let configProvider: ConfigProvider private let randomizer: Randomizer private let sessionStore: SessionStore + private let eventStore: EventStore private let userDefaultStorage: UserDefaultStorage private var previousSessionCrashed = false private let versionCode: String + var shouldReportSession: Bool /// The current session ID. var sessionId: String { @@ -50,6 +53,7 @@ final class BaseSessionManager: SessionManager { configProvider: ConfigProvider, randomizer: Randomizer = BaseRandomizer(), sessionStore: SessionStore, + eventStore: EventStore, userDefaultStorage: UserDefaultStorage, versionCode: String) { self.appBackgroundTimeMs = 0 @@ -59,17 +63,20 @@ final class BaseSessionManager: SessionManager { self.configProvider = configProvider self.randomizer = randomizer self.sessionStore = sessionStore + self.eventStore = eventStore self.userDefaultStorage = userDefaultStorage self.versionCode = versionCode + self.shouldReportSession = false } private func createNewSession() { currentSessionId = idProvider.createId() logger.log(level: .info, message: "New session created", error: nil, data: nil) + shouldReportSession = shouldMarkSessionForExport() let session = SessionEntity(sessionId: sessionId, pid: ProcessInfo.processInfo.processIdentifier, createdAt: Number(timeProvider.now()), - needsReporting: true, + needsReporting: shouldReportSession, crashed: false) sessionStore.insertSession(session) let recentSession = RecentSession(id: session.sessionId, @@ -154,6 +161,11 @@ final class BaseSessionManager: SessionManager { func setPreviousSessionCrashed(_ crashed: Bool) { self.previousSessionCrashed = crashed + if let recentSession = userDefaultStorage.getRecentSession(), previousSessionCrashed { + sessionStore.markCrashedSession(sessionId: recentSession.id) + sessionStore.updateNeedsReporting(sessionId: recentSession.id, needsReporting: true) + eventStore.updateNeedsReportingForAllEvents(sessionId: recentSession.id, needsReporting: true) + } } private func shouldEndSession() -> Bool { @@ -168,12 +180,12 @@ final class BaseSessionManager: SessionManager { } private func shouldMarkSessionForExport() -> Bool { - if configProvider.sessionSamplingRate == 0.0 { + if configProvider.samplingRateForErrorFreeSessions == 0.0 { return false } - if configProvider.sessionSamplingRate == 1.0 { + if configProvider.samplingRateForErrorFreeSessions == 1.0 { return true } - return randomizer.random() < configProvider.sessionSamplingRate + return randomizer.random() < configProvider.samplingRateForErrorFreeSessions } } diff --git a/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents b/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents index 6af05e6ed..523595d86 100644 --- a/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents +++ b/ios/MeasureSDK/XCDataModel/MeasureModel.xcdatamodeld/MeasureModel.xcdatamodel/contents @@ -24,6 +24,7 @@ + diff --git a/ios/MeasureSDKTests/Config/BaseConfigProviderTests.swift b/ios/MeasureSDKTests/Config/BaseConfigProviderTests.swift index 0ef6746ac..2e4ff6b88 100644 --- a/ios/MeasureSDKTests/Config/BaseConfigProviderTests.swift +++ b/ios/MeasureSDKTests/Config/BaseConfigProviderTests.swift @@ -28,27 +28,27 @@ final class BaseConfigProviderTests: XCTestCase { } func testMergedConfigUsesNetworkConfigIfAvailable() { - let networkConfig = Config(enableLogging: false, trackScreenshotOnCrash: true, sessionSamplingRate: 0.2) + let networkConfig = Config(enableLogging: false, trackScreenshotOnCrash: true, samplingRateForErrorFreeSessions: 0.2) mockConfigLoader.networkConfig = networkConfig baseConfigProvider.loadNetworkConfig() - XCTAssertEqual(baseConfigProvider.sessionSamplingRate, 0.2) + XCTAssertEqual(baseConfigProvider.samplingRateForErrorFreeSessions, 0.2) XCTAssertEqual(baseConfigProvider.enableLogging, false) XCTAssertEqual(baseConfigProvider.trackScreenshotOnCrash, true) } func testMergedConfigUsesCachedConfigIfNoNetworkConfig() { - let cachedConfig = Config(enableLogging: true, trackScreenshotOnCrash: true, sessionSamplingRate: 0.15) + let cachedConfig = Config(enableLogging: true, trackScreenshotOnCrash: true, samplingRateForErrorFreeSessions: 0.15) mockConfigLoader.cachedConfig = cachedConfig baseConfigProvider = BaseConfigProvider(defaultConfig: defaultConfig, configLoader: mockConfigLoader) - XCTAssertEqual(baseConfigProvider.sessionSamplingRate, 0.15) + XCTAssertEqual(baseConfigProvider.samplingRateForErrorFreeSessions, 0.15) XCTAssertEqual(baseConfigProvider.enableLogging, true) XCTAssertEqual(baseConfigProvider.trackScreenshotOnCrash, true) } func testMergedConfigUsesDefaultConfigIfNoNetworkOrCachedConfig() { - XCTAssertEqual(baseConfigProvider.sessionSamplingRate, 1.0) + XCTAssertEqual(baseConfigProvider.samplingRateForErrorFreeSessions, 0.0) XCTAssertEqual(baseConfigProvider.enableLogging, false) XCTAssertEqual(baseConfigProvider.trackScreenshotOnCrash, true) XCTAssertEqual(baseConfigProvider.sessionEndLastEventThresholdMs, 1200000) @@ -56,13 +56,13 @@ final class BaseConfigProviderTests: XCTestCase { } func testLoadNetworkConfigUpdatesNetworkConfig() { - let networkConfig = Config(enableLogging: false, trackScreenshotOnCrash: false, sessionSamplingRate: 0.25) + let networkConfig = Config(enableLogging: false, trackScreenshotOnCrash: false, samplingRateForErrorFreeSessions: 0.25) mockConfigLoader.networkConfig = networkConfig baseConfigProvider.loadNetworkConfig() - XCTAssertEqual(baseConfigProvider.sessionSamplingRate, 0.25) + XCTAssertEqual(baseConfigProvider.samplingRateForErrorFreeSessions, 0.25) XCTAssertEqual(baseConfigProvider.enableLogging, false) XCTAssertEqual(baseConfigProvider.trackScreenshotOnCrash, false) XCTAssertEqual(baseConfigProvider.sessionEndLastEventThresholdMs, 1200000) diff --git a/ios/MeasureSDKTests/CoreData/DataCleanupServiceTests.swift b/ios/MeasureSDKTests/CoreData/DataCleanupServiceTests.swift new file mode 100644 index 000000000..d88678a3f --- /dev/null +++ b/ios/MeasureSDKTests/CoreData/DataCleanupServiceTests.swift @@ -0,0 +1,118 @@ +// +// DataCleanupServiceTests.swift +// MeasureSDKTests +// +// Created by Adwin Ross on 15/01/25. +// + +import CoreData +@testable import MeasureSDK +import XCTest + +class DataCleanupServiceTests: XCTestCase { + var coreDataManager: MockCoreDataManager! + var logger: MockLogger! + var eventStore: EventStore! + var sessionStore: SessionStore! + var dataCleanupService: DataCleanupService! + var sessionManager: SessionManager! + + override func setUp() { + super.setUp() + coreDataManager = MockCoreDataManager() + logger = MockLogger() + eventStore = BaseEventStore(coreDataManager: coreDataManager, logger: logger) + sessionStore = BaseSessionStore(coreDataManager: coreDataManager, logger: logger) + sessionManager = MockSessionManager(sessionId: "currentSession") + dataCleanupService = BaseDataCleanupService(eventStore: eventStore, + sessionStore: sessionStore, + logger: logger, + sessionManager: sessionManager) + } + + override func tearDown() { + eventStore = nil + coreDataManager = nil + sessionStore = nil + logger = nil + super.tearDown() + } + + func testDeleteSessionsAndAllEvents() { + let event1 = TestDataGenerator.generateEvents(id: "event1", sessionId: "session1", needsReporting: false) + let event2 = TestDataGenerator.generateEvents(id: "event2", sessionId: "session1", needsReporting: false) + let event3 = TestDataGenerator.generateEvents(id: "event3", sessionId: "session1", needsReporting: false) + let event4 = TestDataGenerator.generateEvents(id: "event4", sessionId: "session1", needsReporting: false) + let session = SessionEntity(sessionId: "session1", pid: 123, createdAt: 123, needsReporting: false, crashed: false) + + eventStore.insertEvent(event: event1) + eventStore.insertEvent(event: event2) + eventStore.insertEvent(event: event3) + eventStore.insertEvent(event: event4) + sessionStore.insertSession(session) + + dataCleanupService.clearStaleData() + + XCTAssertNil(eventStore.getAllEvents()) + XCTAssertNil(sessionStore.getAllSessions()) + } + + func testDeleteSessionsAndEvents_onlyWhereNeedsReportingIsFalse() { + let event1 = TestDataGenerator.generateEvents(id: "event1", sessionId: "session1", needsReporting: false) + let event2 = TestDataGenerator.generateEvents(id: "event2", sessionId: "session1", needsReporting: false) + let event3 = TestDataGenerator.generateEvents(id: "event3", sessionId: "session1", needsReporting: false) + let event4 = TestDataGenerator.generateEvents(id: "event4", sessionId: "session1", needsReporting: true) + let session = SessionEntity(sessionId: "session1", pid: 123, createdAt: 123, needsReporting: false, crashed: false) + + eventStore.insertEvent(event: event1) + eventStore.insertEvent(event: event2) + eventStore.insertEvent(event: event3) + eventStore.insertEvent(event: event4) + sessionStore.insertSession(session) + + dataCleanupService.clearStaleData() + + XCTAssertTrue(eventStore.getAllEvents()?.count == 1) + XCTAssertNil(sessionStore.getAllSessions()) + } + + func testDontDeleteSessionsAndEvents() { + let event1 = TestDataGenerator.generateEvents(id: "event1", sessionId: "session1", needsReporting: true) + let event2 = TestDataGenerator.generateEvents(id: "event2", sessionId: "session1", needsReporting: true) + let event3 = TestDataGenerator.generateEvents(id: "event3", sessionId: "session1", needsReporting: true) + let event4 = TestDataGenerator.generateEvents(id: "event4", sessionId: "session1", needsReporting: true) + let session = SessionEntity(sessionId: "session1", pid: 123, createdAt: 123, needsReporting: true, crashed: false) + + eventStore.insertEvent(event: event1) + eventStore.insertEvent(event: event2) + eventStore.insertEvent(event: event3) + eventStore.insertEvent(event: event4) + sessionStore.insertSession(session) + + dataCleanupService.clearStaleData() + + XCTAssertTrue(eventStore.getAllEvents()?.count == 4) + XCTAssertTrue(sessionStore.getAllSessions()?.count == 1) + } + + func testDontDeleteCurrentSessionAndEvents() { + let event1 = TestDataGenerator.generateEvents(id: "event1", sessionId: "currentSession", needsReporting: false) + let event2 = TestDataGenerator.generateEvents(id: "event2", sessionId: "currentSession", needsReporting: false) + let event3 = TestDataGenerator.generateEvents(id: "event3", sessionId: "session1", needsReporting: false) + let event4 = TestDataGenerator.generateEvents(id: "event4", sessionId: "session1", needsReporting: false) + let currentSession = SessionEntity(sessionId: "currentSession", pid: 123, createdAt: 123, needsReporting: false, crashed: false) + let session = SessionEntity(sessionId: "session1", pid: 1234, createdAt: 1234, needsReporting: false, crashed: false) + + eventStore.insertEvent(event: event1) + eventStore.insertEvent(event: event2) + eventStore.insertEvent(event: event3) + eventStore.insertEvent(event: event4) + sessionStore.insertSession(currentSession) + sessionStore.insertSession(session) + + dataCleanupService.clearStaleData() + + XCTAssertTrue(eventStore.getAllEvents()?.count == 2) + XCTAssertTrue(sessionStore.getAllSessions()?.count == 1) + } +} diff --git a/ios/MeasureSDKTests/CoreData/SessionStoreTests.swift b/ios/MeasureSDKTests/CoreData/SessionStoreTests.swift index 94f01ed54..b398189e8 100644 --- a/ios/MeasureSDKTests/CoreData/SessionStoreTests.swift +++ b/ios/MeasureSDKTests/CoreData/SessionStoreTests.swift @@ -97,7 +97,7 @@ final class SessionStoreTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 5) let sessions = sessionStore.getAllSessions() - XCTAssertEqual(sessions?.count, 0) + XCTAssertNil(sessions) } func testMarkCrashedSessions() { diff --git a/ios/MeasureSDKTests/Event/CustomEventCollectorTests.swift b/ios/MeasureSDKTests/Event/CustomEventCollectorTests.swift index 72ae5e1ff..36aacf725 100644 --- a/ios/MeasureSDKTests/Event/CustomEventCollectorTests.swift +++ b/ios/MeasureSDKTests/Event/CustomEventCollectorTests.swift @@ -49,7 +49,8 @@ final class BaseCustomEventCollectorTests: XCTestCase { XCTAssertNotNil(eventProcessor.data) XCTAssertEqual(eventProcessor.timestamp, 123456789) XCTAssertEqual(eventProcessor.type, .custom) - XCTAssertEqual(eventProcessor.userDefinedAttributes, "{\"is_premium\":true,\"user_name\":\"Alice\"}") + XCTAssertTrue(eventProcessor.userDefinedAttributes!.contains("Alice")) + XCTAssertTrue(eventProcessor.userDefinedAttributes!.contains("true")) } func testTrackEvent_whenDisabled_doesNotSendToProcessor() { diff --git a/ios/MeasureSDKTests/Event/EventProcessorTests.swift b/ios/MeasureSDKTests/Event/EventProcessorTests.swift index d3928509d..410259ba6 100644 --- a/ios/MeasureSDKTests/Event/EventProcessorTests.swift +++ b/ios/MeasureSDKTests/Event/EventProcessorTests.swift @@ -56,7 +56,7 @@ final class EventProcessorTests: XCTestCase { timeProvider = BaseTimeProvider() configProvider = MockConfigProvider(enableLogging: false, trackScreenshotOnCrash: false, - sessionSamplingRate: 1.0, + samplingRateForErrorFreeSessions: 1.0, eventsBatchingIntervalMs: 1000, sessionEndLastEventThresholdMs: 1000, longPressTimeout: 0.5, diff --git a/ios/MeasureSDKTests/Exporter/EventSerializerTests.swift b/ios/MeasureSDKTests/Exporter/EventSerializerTests.swift index df8c011f8..a183fe2bf 100644 --- a/ios/MeasureSDKTests/Exporter/EventSerializerTests.swift +++ b/ios/MeasureSDKTests/Exporter/EventSerializerTests.swift @@ -36,7 +36,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: true ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent should not return nil") @@ -90,7 +90,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: true ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -142,7 +142,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: true ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -209,7 +209,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: true ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -294,7 +294,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: true ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -332,7 +332,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - guard let jsonString = eventSerializer.getSerialisedEvent(for: EventEntity(event)) else { + guard let jsonString = eventSerializer.getSerialisedEvent(for: EventEntity(event, needsReporting: true)) else { XCTFail("getSerialisedEvent cannot be nil") return } @@ -388,7 +388,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -424,7 +424,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -461,7 +461,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -509,7 +509,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -558,7 +558,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -602,7 +602,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -648,7 +648,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -696,7 +696,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo userTriggered: false ) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -750,7 +750,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo attributes: TestDataGenerator.generateAttributes(), userTriggered: false) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") @@ -809,7 +809,7 @@ final class EventSerializerTests: XCTestCase { // swiftlint:disable:this type_bo attributes: TestDataGenerator.generateAttributes(), userTriggered: true) - let eventEntity = EventEntity(event) + let eventEntity = EventEntity(event, needsReporting: true) guard let jsonString = eventSerializer.getSerialisedEvent(for: eventEntity) else { XCTFail("getSerialisedEvent cannot be nil") diff --git a/ios/MeasureSDKTests/Helper/TestDataGenerator.swift b/ios/MeasureSDKTests/Helper/TestDataGenerator.swift index 37ff816ea..55d7167cc 100644 --- a/ios/MeasureSDKTests/Helper/TestDataGenerator.swift +++ b/ios/MeasureSDKTests/Helper/TestDataGenerator.swift @@ -91,7 +91,9 @@ struct TestDataGenerator { hotLaunch: Data? = nil, http: Data? = nil, customEvent: Data? = nil, - networkChange: Data? = nil) -> EventEntity { + networkChange: Data? = nil, + screenView: Data? = nil, + needsReporting: Bool = true) -> EventEntity { return EventEntity( id: id, sessionId: sessionId, @@ -118,7 +120,9 @@ struct TestDataGenerator { hotLaunch: hotLaunch, http: http, networkChange: networkChange, - customEvent: customEvent + customEvent: customEvent, + screenView: screenView, + needsReporting: needsReporting ) } diff --git a/ios/MeasureSDKTests/Mocks/MockConfigProvider.swift b/ios/MeasureSDKTests/Mocks/MockConfigProvider.swift index ac869ba9a..b38722dc4 100644 --- a/ios/MeasureSDKTests/Mocks/MockConfigProvider.swift +++ b/ios/MeasureSDKTests/Mocks/MockConfigProvider.swift @@ -14,7 +14,7 @@ final class MockConfigProvider: ConfigProvider { var maxSessionDurationMs: Number var enableLogging: Bool var trackScreenshotOnCrash: Bool - var sessionSamplingRate: Float + var samplingRateForErrorFreeSessions: Float var eventsBatchingIntervalMs: Number var sessionEndLastEventThresholdMs: Number var longPressTimeout: TimeInterval @@ -29,10 +29,11 @@ final class MockConfigProvider: ConfigProvider { var maxUserDefinedAttributesPerEvent: Int var httpContentTypeAllowlist: [String] var defaultHttpHeadersBlocklist: [String] + var eventTypeExportAllowList: [EventType] init(enableLogging: Bool = false, trackScreenshotOnCrash: Bool = true, - sessionSamplingRate: Float = 1.0, + samplingRateForErrorFreeSessions: Float = 1.0, eventsBatchingIntervalMs: Number = 30000, sessionEndLastEventThresholdMs: Number = 60 * 1000, longPressTimeout: TimeInterval = 500, @@ -54,10 +55,16 @@ final class MockConfigProvider: ConfigProvider { "Set-Cookie", "Proxy-Authorization", "WWW-Authenticate", - "X-Api-Key"]) { + "X-Api-Key"], + eventTypeExportAllowList: [EventType] = [.coldLaunch, + .hotLaunch, + .warmLaunch, + .lifecycleSwiftUI, + .lifecycleViewController, + .screenView]) { self.enableLogging = enableLogging self.trackScreenshotOnCrash = trackScreenshotOnCrash - self.sessionSamplingRate = sessionSamplingRate + self.samplingRateForErrorFreeSessions = samplingRateForErrorFreeSessions self.eventsBatchingIntervalMs = eventsBatchingIntervalMs self.sessionEndLastEventThresholdMs = sessionEndLastEventThresholdMs self.longPressTimeout = longPressTimeout @@ -75,6 +82,7 @@ final class MockConfigProvider: ConfigProvider { self.maxUserDefinedAttributesPerEvent = maxUserDefinedAttributesPerEvent self.httpContentTypeAllowlist = httpContentTypeAllowlist self.defaultHttpHeadersBlocklist = defaultHttpHeadersBlocklist + self.eventTypeExportAllowList = eventTypeExportAllowList } func loadNetworkConfig() {} diff --git a/ios/MeasureSDKTests/Mocks/MockEventStore.swift b/ios/MeasureSDKTests/Mocks/MockEventStore.swift index 856c7c6c1..8a7de60b1 100644 --- a/ios/MeasureSDKTests/Mocks/MockEventStore.swift +++ b/ios/MeasureSDKTests/Mocks/MockEventStore.swift @@ -54,4 +54,17 @@ final class MockEventStore: EventStore { self.events[index].batchId = batchId } } + + func updateNeedsReportingForAllEvents(sessionId: String, needsReporting: Bool) { + for index in 0..