Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(ios): add session sampling and data cleanup #1733

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ios/DemoApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions ios/MeasureSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -369,6 +371,8 @@
5225D0342D0AEB1A00FD240D /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputStream+Extension.swift"; sourceTree = "<group>"; };
5229D16D2CCB533C00EFFE44 /* RecentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSession.swift; sourceTree = "<group>"; };
522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupService.swift; sourceTree = "<group>"; };
522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCleanupServiceTests.swift; sourceTree = "<group>"; };
523287682C85E07B000EE268 /* LifecycleObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleObserverTests.swift; sourceTree = "<group>"; };
523287722C86195E000EE268 /* SessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = "<group>"; };
523287742C8619C4000EE268 /* MockIdProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -803,6 +807,7 @@
children = (
524576722CC116DD00B288E5 /* BatchStore.swift */,
524CC5D22C6A4B48001AB506 /* CoreDataManager.swift */,
522BA9D32D36579000DBF4A3 /* DataCleanupService.swift */,
52A1A94B2CA3D3B000461103 /* Entities */,
52A1A9492CA3CF9B00461103 /* EventStore.swift */,
52A1A93B2CA0777300461103 /* SessionStore.swift */,
Expand Down Expand Up @@ -894,6 +899,7 @@
isa = PBXGroup;
children = (
52D3D7372CC404DA004E404B /* BatchStoreTests.swift */,
522BA9D52D37C2A000DBF4A3 /* DataCleanupServiceTests.swift */,
52A1A94E2CA52C8A00461103 /* EventStoreTests.swift */,
52A1A9402CA087BA00461103 /* SessionStoreTests.swift */,
);
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
8 changes: 6 additions & 2 deletions ios/MeasureSDK/Config/BaseConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class BaseConfigProvider: ConfigProvider {
self.cachedConfig = configLoader.getCachedConfig()
}

var eventTypeExportAllowList: [EventType] {
return getMergedConfig(\.eventTypeExportAllowList)
}

var maxUserDefinedAttributesPerEvent: Int {
return getMergedConfig(\.maxUserDefinedAttributesPerEvent)
}
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 11 additions & 4 deletions ios/MeasureSDK/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
}
}
2 changes: 1 addition & 1 deletion ios/MeasureSDK/Config/DefaultConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions ios/MeasureSDK/Config/InternalConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

}
13 changes: 7 additions & 6 deletions ios/MeasureSDK/Config/MeasureConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
}
}
Expand Down
46 changes: 46 additions & 0 deletions ios/MeasureSDK/CoreData/DataCleanupService.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 6 additions & 2 deletions ios/MeasureSDK/CoreData/Entities/EventEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Codable>(_ event: Event<T>) { // swiftlint:disable:this cyclomatic_complexity function_body_length
init<T: Codable>(_ event: Event<T>, needsReporting: Bool) { // swiftlint:disable:this cyclomatic_complexity function_body_length
self.id = event.id
self.sessionId = event.sessionId
self.timestamp = event.timestamp
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<T: Codable>() -> Event<T> { // swiftlint:disable:this cyclomatic_complexity function_body_length
Expand Down
58 changes: 52 additions & 6 deletions ios/MeasureSDK/CoreData/EventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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 {
Expand All @@ -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] = [:]

Expand Down Expand Up @@ -285,4 +293,42 @@ final class BaseEventStore: EventStore {
}
}
}

func updateNeedsReportingForAllEvents(sessionId: String, needsReporting: Bool) {
let context = coreDataManager.backgroundContext
let fetchRequest: NSFetchRequest<EventOb> = 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> = 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)
}
}
}
}
Loading
Loading