diff --git a/Sources/MixpanelInstance.swift b/Sources/MixpanelInstance.swift index 2eb1c3a2..354b5bc3 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -52,6 +52,8 @@ public protocol MixpanelDelegate: AnyObject { public typealias Properties = [String: MixpanelType] typealias InternalProperties = [String: Any] +typealias TimedEventID = String +typealias TimedEvents = [TimedEventID: TimeInterval] typealias Queue = [InternalProperties] protocol AppLifecycle { @@ -243,7 +245,7 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele var networkQueue: DispatchQueue var optOutStatus: Bool? var useUniqueDistinctId: Bool - var timedEvents = InternalProperties() + var timedEvents = TimedEvents() let readWriteLock: ReadWriteLock #if os(iOS) && !targetEnvironment(macCatalyst) @@ -872,7 +874,7 @@ extension MixpanelInstance { MixpanelPersistence.deleteMPUserDefaultsData(instanceName: self.name) self.readWriteLock.write { - self.timedEvents = InternalProperties() + self.timedEvents = TimedEvents() self.anonymousId = self.defaultDeviceId() self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) self.hadPersistedDistinctId = true @@ -1050,6 +1052,7 @@ extension MixpanelInstance { } extension MixpanelInstance { + // MARK: - Track /** @@ -1065,17 +1068,34 @@ extension MixpanelInstance { - parameter properties: properties dictionary */ public func track(event: String?, properties: Properties? = nil) { + track(event: event, withID: nil, properties: properties) + } + + /** + Tracks an event with properties. + Properties are optional and can be added only if needed. + + Properties will allow you to segment your events in your Mixpanel reports. + Property keys must be String objects and the supported value types need to conform to MixpanelType. + MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull. + If the event is being timed, the timer will stop and be added as a property. + + - parameter event: event name + - parameter withID: eventID used to link timed events previously timed using `time(eventID)` + - parameter properties: properties dictionary + */ + public func track(event: String?, withID eventID: String?, properties: Properties? = nil) { if hasOptedOutTracking() { return } let epochInterval = Date().timeIntervalSince1970 - trackingQueue.async { [weak self, event, properties, epochInterval] in + trackingQueue.async { [weak self, event, eventID, properties, epochInterval] in guard let self = self else { return } - var shadowTimedEvents = InternalProperties() + var shadowTimedEvents = TimedEvents() var shadowSuperProperties = InternalProperties() self.readWriteLock.read { @@ -1090,6 +1110,7 @@ extension MixpanelInstance { alias: nil, hadPersistedDistinctId: self.hadPersistedDistinctId) let timedEventsSnapshot = self.trackInstance.track(event: event, + eventID: eventID, properties: properties, timedEvents: shadowTimedEvents, superProperties: shadowSuperProperties, @@ -1218,10 +1239,36 @@ extension MixpanelInstance { */ public func time(event: String) { + time(eventID: event) + } + + /** + Starts a timer that will be stopped and added as a property when a + corresponding event with the identidal eventID is tracked. + + This method is intended to be used in advance of events that have + a duration. For example, if a developer were to track an "Image Upload" event + she might want to also know how long the upload took. Calling this method + before the upload code would implicitly cause the `track` + call to record its duration. + + - precondition: + // begin timing the image upload: + mixpanelInstance.time(eventID:"some-unique-id") + // upload the image: + self.uploadImageWithSuccessHandler() { _ in + // track the event + mixpanelInstance.track("Image Upload", withID: "some-unique-id") + } + + - parameter eventID: the id of the event to be timed + + */ + public func time(eventID: String) { let startTime = Date().timeIntervalSince1970 - trackingQueue.async { [weak self, startTime, event] in + trackingQueue.async { [weak self, startTime, eventID] in guard let self = self else { return } - let timedEvents = self.trackInstance.time(event: event, timedEvents: self.timedEvents, startTime: startTime) + let timedEvents = self.trackInstance.time(eventID: eventID, timedEvents: self.timedEvents, startTime: startTime) self.readWriteLock.write { self.timedEvents = timedEvents } @@ -1235,12 +1282,21 @@ extension MixpanelInstance { - parameter event: the name of the event to be tracked that was passed to time(event:) */ public func eventElapsedTime(event: String) -> Double { - var timedEvents = InternalProperties() + eventElapsedTime(eventID: event) + } + + /** + Retrieves the time elapsed for the event given it's ID since time(eventID:) was called. + + - parameter event: the id of the event to be tracked that was passed to time(eventID:) + */ + public func eventElapsedTime(eventID: String) -> Double { + var timedEvents = TimedEvents() self.readWriteLock.read { timedEvents = self.timedEvents } - if let startTime = timedEvents[event] as? TimeInterval { + if let startTime = timedEvents[eventID] { return Date().timeIntervalSince1970 - startTime } return 0 @@ -1253,9 +1309,9 @@ extension MixpanelInstance { trackingQueue.async { [weak self] in guard let self = self else { return } self.readWriteLock.write { - self.timedEvents = InternalProperties() + self.timedEvents = TimedEvents() } - MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), instanceName: self.name) + MixpanelPersistence.saveTimedEvents(timedEvents: TimedEvents(), instanceName: self.name) } } @@ -1265,10 +1321,19 @@ extension MixpanelInstance { - parameter event: the name of the event to clear the timer for */ public func clearTimedEvent(event: String) { - trackingQueue.async { [weak self, event] in + clearTimedEvent(eventId: event) + } + + /** + Clears the event timer for the provided eventID. + + - parameter event: the id of the event to clear the timer for + */ + public func clearTimedEvent(eventId: String) { + trackingQueue.async { [weak self, eventId] in guard let self = self else { return } - let updatedTimedEvents = self.trackInstance.clearTimedEvent(event: event, timedEvents: self.timedEvents) + let updatedTimedEvents = self.trackInstance.clearTimedEvent(eventId: eventId, timedEvents: self.timedEvents) MixpanelPersistence.saveTimedEvents(timedEvents: updatedTimedEvents, instanceName: self.name) } } @@ -1521,7 +1586,7 @@ extension MixpanelInstance { self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) self.hadPersistedDistinctId = true self.superProperties = InternalProperties() - MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), instanceName: self.name) + MixpanelPersistence.saveTimedEvents(timedEvents: TimedEvents(), instanceName: self.name) } self.archive() self.readWriteLock.write { diff --git a/Sources/MixpanelPersistence.swift b/Sources/MixpanelPersistence.swift index 24f72dc2..4fdef592 100644 --- a/Sources/MixpanelPersistence.swift +++ b/Sources/MixpanelPersistence.swift @@ -129,7 +129,7 @@ class MixpanelPersistence { return defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") as? Bool } - static func saveTimedEvents(timedEvents: InternalProperties, instanceName: String) { + static func saveTimedEvents(timedEvents: TimedEvents, instanceName: String) { guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { return } @@ -143,19 +143,19 @@ class MixpanelPersistence { } } - static func loadTimedEvents(instanceName: String) -> InternalProperties { + static func loadTimedEvents(instanceName: String) -> TimedEvents { guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return InternalProperties() + return TimedEvents() } let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" guard let timedEventsData = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") else { - return InternalProperties() + return TimedEvents() } do { - return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: timedEventsData) as? InternalProperties ?? InternalProperties() + return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: timedEventsData) as? TimedEvents ?? TimedEvents() } catch { MixpanelLogger.warn(message: "Failed to unarchive timed events") - return InternalProperties() + return TimedEvents() } } @@ -296,7 +296,7 @@ class MixpanelPersistence { peopleQueue: Queue, groupsQueue: Queue, superProperties: InternalProperties, - timedEvents: InternalProperties, + timedEvents: TimedEvents, distinctId: String, anonymousId: String?, userId: String?, @@ -398,7 +398,7 @@ class MixpanelPersistence { } private func unarchiveProperties() -> (InternalProperties, - InternalProperties, + TimedEvents, String, String?, String?, @@ -410,7 +410,7 @@ class MixpanelPersistence { let superProperties = properties?["superProperties"] as? InternalProperties ?? InternalProperties() let timedEvents = - properties?["timedEvents"] as? InternalProperties ?? InternalProperties() + properties?["timedEvents"] as? TimedEvents ?? TimedEvents() let distinctId = properties?["distinctId"] as? String ?? "" let anonymousId = diff --git a/Sources/Track.swift b/Sources/Track.swift index 58a172ac..3ed46f5b 100644 --- a/Sources/Track.swift +++ b/Sources/Track.swift @@ -32,23 +32,25 @@ class Track { } func track(event: String?, + eventID: TimedEventID?, properties: Properties? = nil, - timedEvents: InternalProperties, + timedEvents: TimedEvents, superProperties: InternalProperties, mixpanelIdentity: MixpanelIdentity, - epochInterval: Double) -> InternalProperties { - var ev = "mp_event" - if let event = event { - ev = event + epochInterval: Double) -> TimedEvents { + var eventName = "mp_event" + if let event { + eventName = event } else { MixpanelLogger.info(message: "mixpanel track called with empty event parameter. using 'mp_event'") } - if !(mixpanelInstance?.trackAutomaticEventsEnabled ?? false) && ev.hasPrefix("$ae_") { + if !(mixpanelInstance?.trackAutomaticEventsEnabled ?? false) && eventName.hasPrefix("$ae_") { return timedEvents } + let timedEventID = eventID ?? eventName assertPropertyTypes(properties) let epochMilliseconds = round(epochInterval * 1000) - let eventStartTime = timedEvents[ev] as? Double + let eventStartTime = timedEvents[timedEventID] var p = InternalProperties() AutomaticProperties.automaticPropertiesLock.read { p += AutomaticProperties.properties @@ -57,7 +59,7 @@ class Track { p["time"] = epochMilliseconds var shadowTimedEvents = timedEvents if let eventStartTime = eventStartTime { - shadowTimedEvents.removeValue(forKey: ev) + shadowTimedEvents.removeValue(forKey: timedEventID) p["$duration"] = Double(String(format: "%.3f", epochInterval - eventStartTime)) } p["distinct_id"] = mixpanelIdentity.distinctID @@ -76,7 +78,7 @@ class Track { p += properties } - var trackEvent: InternalProperties = ["event": ev, "properties": p] + var trackEvent: InternalProperties = ["event": eventName, "properties": p] metadata.toDict().forEach { (k, v) in trackEvent[k] = v } self.mixpanelPersistence.saveEntity(trackEvent, type: .events) @@ -134,32 +136,32 @@ class Track { update(&superProperties) } - func time(event: String?, timedEvents: InternalProperties, startTime: Double) -> InternalProperties { + func time(eventID: TimedEventID, timedEvents: TimedEvents, startTime: TimeInterval) -> TimedEvents { if mixpanelInstance?.hasOptedOutTracking() ?? false { return timedEvents } var updatedTimedEvents = timedEvents - guard let event = event, !event.isEmpty else { + guard !eventID.isEmpty else { MixpanelLogger.error(message: "mixpanel cannot time an empty event") return updatedTimedEvents } - updatedTimedEvents[event] = startTime + updatedTimedEvents[eventID] = startTime return updatedTimedEvents } - func clearTimedEvents(_ timedEvents: InternalProperties) -> InternalProperties { + func clearTimedEvents(_ timedEvents: TimedEvents) -> TimedEvents { var updatedTimedEvents = timedEvents updatedTimedEvents.removeAll() return updatedTimedEvents } - func clearTimedEvent(event: String?, timedEvents: InternalProperties) -> InternalProperties { + func clearTimedEvent(eventId: TimedEventID, timedEvents: TimedEvents) -> TimedEvents { var updatedTimedEvents = timedEvents - guard let event = event, !event.isEmpty else { + guard !eventId.isEmpty else { MixpanelLogger.error(message: "mixpanel cannot clear an empty timed event") return updatedTimedEvents } - updatedTimedEvents.removeValue(forKey: event) + updatedTimedEvents.removeValue(forKey: eventId) return updatedTimedEvents } }