Skip to content

Commit

Permalink
chore(ios): add custom event tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
Adwin Ronald Ross committed Dec 31, 2024
1 parent cd54d5e commit f4abe89
Show file tree
Hide file tree
Showing 20 changed files with 252 additions and 7 deletions.
1 change: 1 addition & 0 deletions ios/DemoApp/Controller/ObjcDetailViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ - (void)viewDidLoad {

UIView *headerView = [self createTableHeaderView];
tableView.tableHeaderView = headerView;
[[Measure shared] trackEventWithName:@"event-name" timestamp:nil];

[self setTitle:@"Objc View Controller"];

Expand Down
5 changes: 5 additions & 0 deletions ios/DemoApp/Controller/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ import MeasureSDK
view.addSubview(tableView)
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
Measure.shared.trackEvent(name: "HomeVC-custom-event", timestamp: nil)
}

// MARK: - Table Header View with Buttons

func createTableHeaderView() -> UIView {
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 @@ -106,6 +106,8 @@
52AE72032CABAE9000F2830A /* GestureEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52AE71FD2CABAE9000F2830A /* GestureEvents.swift */; };
52AE72082CABAEAB00F2830A /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52AE72062CABAEAB00F2830A /* UIWindow+Extension.swift */; };
52AE72092CABAEAB00F2830A /* NSObject+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52AE72072CABAEAB00F2830A /* NSObject+Extension.swift */; };
52B2A8772D1A790200C6B5CF /* CustomEventData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B2A8762D1A790200C6B5CF /* CustomEventData.swift */; };
52B2A8792D1A89EF00C6B5CF /* CustomEventCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B2A8782D1A89EF00C6B5CF /* CustomEventCollector.swift */; };
52BCF1DC2CB42026003102DF /* MeasureModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 52BCF1DA2CB42026003102DF /* MeasureModel.xcdatamodeld */; };
52BCF1DD2CB42383003102DF /* MeasureModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 52BCF1DA2CB42026003102DF /* MeasureModel.xcdatamodeld */; };
52BCF20A2CB59FBF003102DF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BCF2092CB59FBF003102DF /* AppDelegate.swift */; };
Expand Down Expand Up @@ -408,6 +410,8 @@
52AE71FD2CABAE9000F2830A /* GestureEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureEvents.swift; sourceTree = "<group>"; };
52AE72062CABAEAB00F2830A /* UIWindow+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = "<group>"; };
52AE72072CABAEAB00F2830A /* NSObject+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+Extension.swift"; sourceTree = "<group>"; };
52B2A8762D1A790200C6B5CF /* CustomEventData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventData.swift; sourceTree = "<group>"; };
52B2A8782D1A89EF00C6B5CF /* CustomEventCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventCollector.swift; sourceTree = "<group>"; };
52BCF1DB2CB42026003102DF /* MeasureModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeasureModel.xcdatamodel; sourceTree = "<group>"; };
52BCF2072CB59FBF003102DF /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
52BCF2092CB59FBF003102DF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -591,6 +595,8 @@
children = (
5202BE732C8B117900A3496E /* Attachment.swift */,
5202BE742C8B117900A3496E /* AttachmentType.swift */,
52B2A8782D1A89EF00C6B5CF /* CustomEventCollector.swift */,
52B2A8762D1A790200C6B5CF /* CustomEventData.swift */,
5202BE752C8B117900A3496E /* Event.swift */,
5202BE762C8B117900A3496E /* EventProcessor.swift */,
5202BE772C8B117900A3496E /* EventType.swift */,
Expand Down Expand Up @@ -1353,6 +1359,7 @@
52AE72022CABAE9000F2830A /* GestureDetector.swift in Sources */,
52CC63C12C9C608E00F7CA0A /* CrashDataPersistence.swift in Sources */,
52EB380C2C8C7334002D63EC /* SignPost.swift in Sources */,
52B2A8792D1A89EF00C6B5CF /* CustomEventCollector.swift in Sources */,
52A8533E2C994CC200B2A39F /* ExceptionDetail.swift in Sources */,
526E30F62CE77BCB00F484B4 /* AppLaunchEvents.swift in Sources */,
52A853402C994D7900B2A39F /* Exception.swift in Sources */,
Expand All @@ -1375,6 +1382,7 @@
524576712CC1128600B288E5 /* BatchCreator.swift in Sources */,
52F93B9E2CAE6B3C00168AB4 /* GestureTargetFinder.swift in Sources */,
52D51ABB2CD0E5C9008F30A6 /* MsrMoniterView.swift in Sources */,
52B2A8772D1A790200C6B5CF /* CustomEventData.swift in Sources */,
524576732CC116DD00B288E5 /* BatchStore.swift in Sources */,
52816B6A2CCE399B00B160A4 /* LifecycleCollector.swift in Sources */,
528EAB962C84553500CB1574 /* LifecycleObserver.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions ios/MeasureSDK/Config/BaseConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ final class BaseConfigProvider: ConfigProvider {
self.cachedConfig = configLoader.getCachedConfig()
}

var maxEventNameLength: Int {
return getMergedConfig(\.maxEventNameLength)
}

var customEventNameRegex: String {
return getMergedConfig(\.customEventNameRegex)
}

var maxSessionDurationMs: Number {
return getMergedConfig(\.maxSessionDurationMs)
}
Expand Down
4 changes: 4 additions & 0 deletions ios/MeasureSDK/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ struct Config: InternalConfig, MeasureConfig {
let maxSessionDurationMs: Number
let cpuTrackingIntervalMs: UnsignedNumber
let memoryTrackingIntervalMs: UnsignedNumber
let customEventNameRegex: String
let maxEventNameLength: Int

internal init(enableLogging: Bool = DefaultConfig.enableLogging,
trackScreenshotOnCrash: Bool = DefaultConfig.trackScreenshotOnCrash,
Expand All @@ -45,5 +47,7 @@ struct Config: InternalConfig, MeasureConfig {
self.maxSessionDurationMs = 6 * 60 * 60 * 1000 // 6 hours
self.cpuTrackingIntervalMs = 3 * 1000 // 3 seconds
self.memoryTrackingIntervalMs = 2 * 1000 // 2 seconds
self.customEventNameRegex = "^[a-zA-Z0-9_-]"
self.maxEventNameLength = 64 // 64 chars
}
}
6 changes: 6 additions & 0 deletions ios/MeasureSDK/Config/InternalConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ protocol InternalConfig {

/// The interval at which memory related data is collected. Defaults to 2 seconds.
var memoryTrackingIntervalMs: UnsignedNumber { get }

/// The maximum length of a custom event. Defaults to 64 chars.
var maxEventNameLength: Int { get }

/// The regex to validate a custom event name.
var customEventNameRegex: String { get }
}
24 changes: 23 additions & 1 deletion ios/MeasureSDK/CoreData/Entities/EventEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct EventEntity {
let attachmentSize: Number
let timestampInMillis: Number
var batchId: String?
var customEvent: Data?

init<T: Codable>(_ event: Event<T>) { // swiftlint:disable:this cyclomatic_complexity function_body_length
self.id = event.id
Expand Down Expand Up @@ -173,6 +174,17 @@ struct EventEntity {
self.hotLaunch = nil
}

if let customEvent = event.data as? CustomEventData {
do {
let data = try JSONEncoder().encode(customEvent)
self.customEvent = data
} catch {
self.customEvent = nil
}
} else {
self.customEvent = nil
}

if let attributes = event.attributes {
do {
let data = try JSONEncoder().encode(attributes)
Expand Down Expand Up @@ -213,7 +225,8 @@ struct EventEntity {
memoryUsage: Data?,
coldLaunch: Data?,
warmLaunch: Data?,
hotLaunch: Data?) {
hotLaunch: Data?,
customEvent: Data?) {
self.id = id
self.sessionId = sessionId
self.timestamp = timestamp
Expand All @@ -236,6 +249,7 @@ struct EventEntity {
self.coldLaunch = coldLaunch
self.warmLaunch = warmLaunch
self.hotLaunch = hotLaunch
self.customEvent = customEvent
}

func getEvent<T: Codable>() -> Event<T> { // swiftlint:disable:this cyclomatic_complexity function_body_length
Expand Down Expand Up @@ -338,6 +352,14 @@ struct EventEntity {
decodedData = nil
}
}
case .custom:
if let customEventData = self.customEvent {
do {
decodedData = try JSONDecoder().decode(T.self, from: customEventData)
} catch {
decodedData = nil
}
}
case nil:
decodedData = nil
}
Expand Down
10 changes: 7 additions & 3 deletions ios/MeasureSDK/CoreData/EventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class BaseEventStore: EventStore {
eventOb.coldLaunch = event.coldLaunch
eventOb.warmLaunch = event.warmLaunch
eventOb.hotLaunch = event.hotLaunch
eventOb.customEvent = event.customEvent

do {
try context.saveIfNeeded()
Expand Down Expand Up @@ -93,7 +94,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch)
hotLaunch: eventOb.hotLaunch,
customEvent: eventOb.customEvent)
}
} catch {
guard let self = self else { return }
Expand Down Expand Up @@ -134,7 +136,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch)
hotLaunch: eventOb.hotLaunch,
customEvent: eventOb.customEvent)
}
} catch {
guard let self = self else { return }
Expand Down Expand Up @@ -194,7 +197,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch))
hotLaunch: eventOb.hotLaunch,
customEvent: eventOb.customEvent))
}
} catch {
guard let self = self else {
Expand Down
72 changes: 72 additions & 0 deletions ios/MeasureSDK/Events/CustomEventCollector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// CustomEventCollector.swift
// MeasureSDK
//
// Created by Adwin Ross on 24/12/24.
//

import Foundation

protocol CustomEventCollector {
func enable()
func disable()
func trackEvent(name: String, timestamp: Number?)
}

final class BaseCustomEventCollector: CustomEventCollector {
private let logger: Logger
private let eventProcessor: EventProcessor
private let timeProvider: TimeProvider
private let configProvider: ConfigProvider
private var isEnabled = false
private lazy var customEventNameRegex: NSRegularExpression? = {
try? NSRegularExpression(pattern: configProvider.customEventNameRegex)
}()

init(logger: Logger, eventProcessor: EventProcessor, timeProvider: TimeProvider, configProvider: ConfigProvider) {
self.logger = logger
self.eventProcessor = eventProcessor
self.timeProvider = timeProvider
self.configProvider = configProvider
}

func enable() {
isEnabled = true
}

func disable() {
isEnabled = false
}

func trackEvent(name: String, timestamp: Number?) {
guard isEnabled else { return }
guard validateName(name) else { return }

let data = CustomEventData(name: name)
eventProcessor.track(data: data,
timestamp: timeProvider.now(),
type: .custom,
attributes: nil,
sessionId: nil,
attachments: nil)
}

private func validateName(_ name: String) -> Bool {
if name.isEmpty {
logger.log(level: .warning, message: "Event name is empty. This event will be dropped.", error: nil, data: nil)
return false
}

if name.count > configProvider.maxEventNameLength {
logger.log(level: .warning, message: "Event(\(name)) exceeded max allowed length. This event will be dropped.", error: nil, data: nil)
return false
}

if let regex = customEventNameRegex,
regex.firstMatch(in: name, options: [], range: NSRange(location: 0, length: name.count)) == nil {
return false
}

return true
}
}
12 changes: 12 additions & 0 deletions ios/MeasureSDK/Events/CustomEventData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// CustomEventData.swift
// MeasureSDK
//
// Created by Adwin Ross on 23/12/24.
//

import Foundation

struct CustomEventData: Codable {
let name: String
}
1 change: 1 addition & 0 deletions ios/MeasureSDK/Events/EventType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ enum EventType: String, Codable {
case coldLaunch = "cold_launch"
case warmLaunch = "warm_launch"
case hotLaunch = "hot_launch"
case custom
}
17 changes: 17 additions & 0 deletions ios/MeasureSDK/Exporter/EventSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ struct EventSerializer {
}
}
return nil
case .custom:
if let customEventData = event.customEvent {
do {
let decodedData = try JSONDecoder().decode(CustomEventData.self, from: customEventData)
return serialiseCustomEventData(decodedData)
} catch {
return nil
}
}
return nil
case nil:
return nil
}
Expand Down Expand Up @@ -321,6 +331,13 @@ struct EventSerializer {
return result
}

private func serialiseCustomEventData(_ customEventData: CustomEventData) -> String {
var result = "{"
result += "\"custom\":\"\(customEventData.name)\""
result += "}"
return result
}

private func getSerialisedAttributes(for event: EventEntity) -> String? {
let decodedAttributes: Attributes?
if let attributeData = event.attributes {
Expand Down
25 changes: 25 additions & 0 deletions ios/MeasureSDK/Measure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,29 @@ import Foundation
}
}
}

/// Tracks an event with optional timestamp.
///
/// Usage Notes:
/// - Event names should be clear and consistent to aid in dashboard searches
///
/// /// - Example:
/// - Swift:
/// ```swift
/// Measure.shared.trackEvent(name: "event-name", timestamp: nil)
/// ```
/// - Objective-C:
/// ```objc
/// [[Measure shared] trackEventWithName:@"event-name" timestamp:nil];
///
/// - Parameters:
/// - name: Name of the event (max 64 characters)
/// - timestamp: Optional timestamp for the event, defaults to current time
///
@objc public func trackEvent(name: String, timestamp: NSNumber?) {
let timestampValue: Int64? = timestamp?.int64Value
guard let customEventCollector = measureInternal?.customEventCollector else { return }

customEventCollector.trackEvent(name: name, timestamp: timestampValue)
}
}
7 changes: 7 additions & 0 deletions ios/MeasureSDK/MeasureInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ protocol MeasureInitializer {
var memoryUsageCalculator: MemoryUsageCalculator { get }
var sysCtl: SysCtl { get }
var appLaunchCollector: AppLaunchCollector { get }
var customEventCollector: CustomEventCollector { get }
}

/// `BaseMeasureInitializer` is responsible for setting up the internal configuration
Expand Down Expand Up @@ -81,6 +82,7 @@ protocol MeasureInitializer {
/// - `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.
/// - `customEventCollector`: `CustomEventCollector` object that triggers custom events.
/// - `sysCtl`: `SysCtl` object which provides sysctl functionalities.
/// - `httpClient`: `HttpClient` object that handles HTTP requests.
/// - `networkClient`: `NetworkClient` object is responsible for initializing the network configuration and executing API requests.
Expand Down Expand Up @@ -128,6 +130,7 @@ final class BaseMeasureInitializer: MeasureInitializer {
let memoryUsageCalculator: MemoryUsageCalculator
let sysCtl: SysCtl
let appLaunchCollector: AppLaunchCollector
let customEventCollector: CustomEventCollector

init(config: MeasureConfig, // swiftlint:disable:this function_body_length
client: Client) {
Expand Down Expand Up @@ -236,6 +239,10 @@ final class BaseMeasureInitializer: MeasureInitializer {
sysCtl: sysCtl,
userDefaultStorage: userDefaultStorage,
currentAppVersion: appVersion)
self.customEventCollector = BaseCustomEventCollector(logger: logger,
eventProcessor: eventProcessor,
timeProvider: timeProvider,
configProvider: configProvider)
self.client = client
}
}
Loading

0 comments on commit f4abe89

Please sign in to comment.