Skip to content

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
91 changes: 78 additions & 13 deletions Sources/MixpanelInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public protocol MixpanelDelegate: AnyObject {

public typealias Properties = [String: MixpanelType]
typealias InternalProperties = [String: Any]
typealias TimedEventID = String
typealias TimedEvents = [TimedEventID: TimeInterval]
Comment on lines +55 to +56
Copy link
Author

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.

typealias Queue = [InternalProperties]

protocol AppLifecycle {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1002,6 +1004,7 @@ extension MixpanelInstance {
}

extension MixpanelInstance {

// MARK: - Track

/**
Expand All @@ -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)
Copy link
Author

Choose a reason for hiding this comment

The 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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions Sources/MixpanelPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Logger.warn(message: "Failed to unarchive timed events")
return InternalProperties()
return TimedEvents()
}
}

Expand Down Expand Up @@ -296,7 +296,7 @@ class MixpanelPersistence {
peopleQueue: Queue,
groupsQueue: Queue,
superProperties: InternalProperties,
timedEvents: InternalProperties,
timedEvents: TimedEvents,
distinctId: String,
anonymousId: String?,
userId: String?,
Expand Down Expand Up @@ -398,7 +398,7 @@ class MixpanelPersistence {
}

private func unarchiveProperties() -> (InternalProperties,
InternalProperties,
TimedEvents,
String,
String?,
String?,
Expand All @@ -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 =
Expand Down
36 changes: 19 additions & 17 deletions Sources/Track.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Copy link
Author

@cmriboldi cmriboldi Jan 22, 2024

Choose a reason for hiding this comment

The 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
}
}