-
Notifications
You must be signed in to change notification settings - Fork 238
Add eventID's to time and track methods for more reliable durations. #625
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
3d19f1f
b953aa7
a512be1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,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 { | ||
|
@@ -210,7 +212,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) | ||
|
@@ -819,7 +821,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 | ||
|
@@ -1002,6 +1004,7 @@ extension MixpanelInstance { | |
} | ||
|
||
extension MixpanelInstance { | ||
|
||
// MARK: - Track | ||
|
||
/** | ||
|
@@ -1017,17 +1020,34 @@ extension MixpanelInstance { | |
- parameter properties: properties dictionary | ||
*/ | ||
public func track(event: String?, properties: Properties? = nil) { | ||
track(event: event, withID: nil, properties: properties) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call site for all of these methods remains the same for backwards compatibility, and under the hood they just call the other method that uses the event id. |
||
} | ||
|
||
/** | ||
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 { | ||
|
@@ -1042,6 +1062,7 @@ extension MixpanelInstance { | |
alias: nil, | ||
hadPersistedDistinctId: self.hadPersistedDistinctId) | ||
let timedEventsSnapshot = self.trackInstance.track(event: event, | ||
eventID: eventID, | ||
properties: properties, | ||
timedEvents: shadowTimedEvents, | ||
superProperties: shadowSuperProperties, | ||
|
@@ -1170,10 +1191,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 | ||
} | ||
|
@@ -1187,12 +1234,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 | ||
|
@@ -1205,9 +1261,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) | ||
} | ||
} | ||
|
||
|
@@ -1217,10 +1273,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) | ||
} | ||
} | ||
|
@@ -1473,7 +1538,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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,28 +32,30 @@ 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 { | ||
Logger.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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If no eventID is provided we fall back to using whatever the eventName is which means we default back to the original behavior. |
||
assertPropertyTypes(properties) | ||
#if DEBUG | ||
if !ev.hasPrefix("$") { | ||
if !eventName.hasPrefix("$") { | ||
UserDefaults.standard.set(true, forKey: InternalKeys.mpDebugTrackedKey) | ||
} | ||
#endif | ||
let epochMilliseconds = round(epochInterval * 1000) | ||
let eventStartTime = timedEvents[ev] as? Double | ||
let eventStartTime = timedEvents[timedEventID] | ||
var p = InternalProperties() | ||
AutomaticProperties.automaticPropertiesLock.read { | ||
p += AutomaticProperties.properties | ||
|
@@ -62,7 +64,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 | ||
|
@@ -81,7 +83,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) | ||
|
@@ -139,32 +141,32 @@ class Track { | |
update(&superProperties) | ||
} | ||
|
||
func time(event: String?, timedEvents: InternalProperties, startTime: Double) -> InternalProperties { | ||
func time(eventID: TimedEventID, timedEvents: TimedEvents, startTime: TimeInterval) -> TimedEvents { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I renamed the property name for clarity, and changed the types for consistency/correctness and removed the optionality of the eventID because it didn't need to be optional. |
||
if mixpanelInstance?.hasOptedOutTracking() ?? false { | ||
return timedEvents | ||
} | ||
var updatedTimedEvents = timedEvents | ||
guard let event = event, !event.isEmpty else { | ||
guard !eventID.isEmpty else { | ||
Logger.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 { | ||
Logger.error(message: "mixpanel cannot clear an empty timed event") | ||
return updatedTimedEvents | ||
} | ||
updatedTimedEvents.removeValue(forKey: event) | ||
updatedTimedEvents.removeValue(forKey: eventId) | ||
return updatedTimedEvents | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added these two typealiases for additional type-safely and clarity. Ideally TimedEventID is it's own type that wraps a string but I didn't want to change the call site too drastically from what it already is.