diff --git a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift index 81de8f6e..84d44df1 100644 --- a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift +++ b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift @@ -15,7 +15,8 @@ class TrackingViewController: UIViewController, UITableViewDelegate, UITableView var tableViewItems = [ "Track w/o Properties", "Track w Properties", - "Time Event 5secs", + "Time Event 5secs with name", + "Time Event 5secs with ID", "Clear Timed Events", "Get Current SuperProperties", "Clear SuperProperties", @@ -57,22 +58,30 @@ class TrackingViewController: UIViewController, UITableViewDelegate, UITableView Mixpanel.mainInstance().track(event: ev, properties: p) descStr = "Event: \"\(ev)\"\n Properties: \(p)" case 2: - let ev = "Timed Event" + let ev = "Timed Event with name" Mixpanel.mainInstance().time(event: ev) DispatchQueue.main.asyncAfter(deadline: .now() + 5) { Mixpanel.mainInstance().track(event: ev) } descStr = "Timed Event: \"\(ev)\"" case 3: + let evID = UUID() + let ev = "Timed Event with id" + Mixpanel.mainInstance().time(timedEventID: evID) + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Mixpanel.mainInstance().track(event: ev, timedEventID: evID) + } + descStr = "Timed Event: \"\(ev)\"" + case 4: Mixpanel.mainInstance().clearTimedEvents() descStr = "Timed Events Cleared" - case 4: + case 5: descStr = "Super Properties:\n" descStr += "\(Mixpanel.mainInstance().currentSuperProperties())" - case 5: + case 6: Mixpanel.mainInstance().clearSuperProperties() descStr = "Cleared Super Properties" - case 6: + case 7: let p: Properties = [ "Super Property 1": 1, "Super Property 2": "p2", @@ -85,15 +94,15 @@ class TrackingViewController: UIViewController, UITableViewDelegate, UITableView ] Mixpanel.mainInstance().registerSuperProperties(p) descStr = "Properties: \(p)" - case 7: + case 8: let p = ["Super Property 1": 2.3] Mixpanel.mainInstance().registerSuperPropertiesOnce(p) descStr = "Properties: \(p)" - case 8: + case 9: let p = ["Super Property 1": 1.2] Mixpanel.mainInstance().registerSuperPropertiesOnce(p, defaultValue: 2.3) descStr = "Properties: \(p) with Default Value: 2.3" - case 9: + case 10: let p = "Super Property 2" Mixpanel.mainInstance().unregisterSuperProperty(p) descStr = "Properties: \(p)" diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift index b999d368..fec64c01 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift @@ -89,6 +89,10 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { func randomId() -> String { return String(format: "%08x%08x", arc4random(), arc4random()) } + + func uuid(_ intValue: Int) -> UUID { + return UUID.init(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", intValue))")! + } func waitForAsyncTasks() { var hasCompletedTask = false @@ -103,20 +107,20 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { } func eventQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .events) + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .events) } func peopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people) + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .people) } func unIdentifiedPeopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch( + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch( type: .people, flag: PersistenceConstant.unIdentifiedFlag) } func groupQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .groups) + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .groups) } func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift index 922b419f..ea329d4e 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift @@ -131,37 +131,37 @@ class MixpanelDemoTests: MixpanelBaseTests { removeDBfile(testMixpanel) } - func testAddEventContainsInvalidJsonObjectDoubleNaN() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) - } - removeDBfile(testMixpanel) - } - - func testAddEventContainsInvalidJsonObjectFloatNaN() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) - } - removeDBfile(testMixpanel) - } - - func testAddEventContainsInvalidJsonObjectDoubleInfinity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) - } - removeDBfile(testMixpanel) - } - - func testAddEventContainsInvalidJsonObjectFloatInfinity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) - } - removeDBfile(testMixpanel) - } + func testAddEventContainsInvalidJsonObjectDoubleNaN() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) + } + removeDBfile(testMixpanel) + } + + func testAddEventContainsInvalidJsonObjectFloatNaN() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) + } + removeDBfile(testMixpanel) + } + + func testAddEventContainsInvalidJsonObjectDoubleInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) + } + removeDBfile(testMixpanel) + } + + func testAddEventContainsInvalidJsonObjectFloatInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) + } + removeDBfile(testMixpanel) + } func testAddingEventsAfterFlush() { let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) @@ -349,7 +349,7 @@ class MixpanelDemoTests: MixpanelBaseTests { waitForTrackingQueue(testMixpanel) testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) waitForTrackingQueue(testMixpanel) - var mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + var mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) XCTAssertTrue( distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) @@ -357,16 +357,16 @@ class MixpanelDemoTests: MixpanelBaseTests { waitForTrackingQueue(testMixpanel) testMixpanel.unarchive() waitForTrackingQueue(testMixpanel) - mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) XCTAssertTrue( testMixpanel.distinctId == mixpanelIdentity.distinctID && testMixpanel.people.distinctId == mixpanelIdentity.peopleDistinctID && testMixpanel.anonymousId == mixpanelIdentity.anonymousId && testMixpanel.userId == mixpanelIdentity.userId && testMixpanel.alias == mixpanelIdentity.alias) - MixpanelPersistence.deleteMPUserDefaultsData(apiToken: testMixpanel.apiToken) + MixpanelPersistence.deleteMPUserDefaultsData(instanceName: testMixpanel.apiToken) waitForTrackingQueue(testMixpanel) - mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) XCTAssertTrue( "" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId @@ -778,7 +778,7 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertTrue( peopleQueue(token: testMixpanel2.apiToken).count == 1, "pending people queue archive failed") XCTAssertEqual( - testMixpanel2.timedEvents["e2"] as? Int, 5, + testMixpanel2.timedEvents["e2"], 5, "timedEvents archive failed") testMixpanel2.mixpanelPersistence.closeDB() let testMixpanel3 = Mixpanel.initialize(token: testToken, flushInterval: 60) @@ -848,20 +848,111 @@ class MixpanelDemoTests: MixpanelBaseTests { testMixpanel.time(event: "Time Event B") testMixpanel.time(event: "Time Event C") waitForTrackingQueue(testMixpanel) - var testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) XCTAssertTrue( testTimedEvents.count == 3, "Each call to time() should add an event to timedEvents") XCTAssertNotNil(testTimedEvents["Time Event A"], "Keys in timedEvents should be event names") testMixpanel.clearTimedEvent(event: "Time Event A") waitForTrackingQueue(testMixpanel) - testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) XCTAssertNil(testTimedEvents["Time Event A"], "clearTimedEvent should remove key/value pair") XCTAssertTrue( testTimedEvents.count == 2, "clearTimedEvent shoud remove only one key/value pair") testMixpanel.clearTimedEvents() waitForTrackingQueue(testMixpanel) XCTAssertTrue( - MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken).count == 0, + MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken).count == 0, + "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel) + } + + func testEventTimingWithTimedEventID() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + var e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + var p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "New events should not be timed.") + let event1UUID = uuid(1) + let event2UUID = uuid(2) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.track(event: "Event 1", timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "The exact same timedEventID is required for timing, regardless of name") + testMixpanel.track(event: "Event 1", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$duration"], "This event should be timed.") + testMixpanel.track(event: "Event 1", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil( + p["$duration"], + "Tracking the same event should require a second call to timeEvent.") + removeDBfile(testMixpanel) + } + + func testClearingTimedEventsWithId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let event1UUID = uuid(1) + let event2UUID = uuid(2) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.time(timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 2, "Each call to time() should add an event to timedEvents") + XCTAssertNotNil(testTimedEvents[event1UUID.uuidString], "Keys in timedEvents should be timedEventID's uuidString") + XCTAssertNotNil(testTimedEvents[event2UUID.uuidString], "Keys in timedEvents should be timedEventID's uuidString") + testMixpanel.clearTimedEvent(timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertNil(testTimedEvents[event1UUID.uuidString], "clearTimedEvent should remove key/value pair") + XCTAssertNotNil(testTimedEvents[event2UUID.uuidString], "clearTimedEvent should only remove the correct timedEventID's uuidString") + XCTAssertTrue( + testTimedEvents.count == 1, "clearTimedEvent shoud remove only one key/value pair") + removeDBfile(testMixpanel) + } + + func testEventTimingMultipleTimedEventsWithId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let event1UUID = uuid(1) + let event2UUID = uuid(2) + let event3UUID = uuid(3) + let event4UUID = uuid(4) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.time(timedEventID: event2UUID) + testMixpanel.time(timedEventID: event3UUID) + testMixpanel.time(timedEventID: event4UUID) + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 4, "Each call to time() should add an event to timedEvents") + testMixpanel.track(event: "Recurring Event", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + let event4 = eventQueue(token: testMixpanel.apiToken).last! + let event4Properties = event4["properties"] as! InternalProperties + XCTAssertNotNil(event4Properties["$duration"], "This event should be timed.") + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 3, "tracking and event shoud remove only one key/value pair") + testMixpanel.track(event: "Recurring Event", timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + let event5 = eventQueue(token: testMixpanel.apiToken).last! + let event5Properties = event5["properties"] as! InternalProperties + XCTAssertNotNil( + event5Properties["$duration"], "This event should be timed. Event though there was another event with the same name.") + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 2, "tracking and event shoud remove only one key/value pair") + testMixpanel.clearTimedEvents() + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue(testTimedEvents.count == 0, "clearTimedEvents should remove all key/value pairs") removeDBfile(testMixpanel) } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift index 52ea54be..24a5c540 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift @@ -262,6 +262,20 @@ class MixpanelOptOutTests: MixpanelBaseTests { "When opted out, this event should not be timed.") removeDBfile(testMixpanel) } + + func testOptOutWilSkipTimeEventWithId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + let eventUUID = uuid(1) + testMixpanel.time(timedEventID: eventUUID) + testMixpanel.track(event: "400 Meters", timedEventID: eventUUID) + waitForTrackingQueue(testMixpanel) + XCTAssertNil( + eventQueue(token: testMixpanel.apiToken).last, + "When opted out, this event should not be timed.") + removeDBfile(testMixpanel) + } func testOptOutWillSkipFlushPeople() { let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift index ef55c19b..6720e14a 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift @@ -84,6 +84,10 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { func randomId() -> String { return String(format: "%08x%08x", arc4random(), arc4random()) } + + func uuid(_ intValue: Int) -> UUID { + return UUID.init(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", intValue))")! + } func waitForAsyncTasks() { var hasCompletedTask = false diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift index faf0b5ba..d3e27605 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift @@ -978,7 +978,7 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertTrue( peopleQueue(token: testMixpanel2.apiToken).count >= 1, "pending people queue archive failed") XCTAssertEqual( - testMixpanel2.timedEvents["e2"] as? Int, 5, + testMixpanel2.timedEvents["e2"], 5, "timedEvents archive failed") let testMixpanel3 = Mixpanel.initialize( token: testToken, trackAutomaticEvents: false, flushInterval: 60) @@ -1066,6 +1066,97 @@ class MixpanelDemoTests: MixpanelBaseTests { "clearTimedEvents should remove all key/value pairs") removeDBfile(testMixpanel.apiToken) } + + func testEventTimingWithTimedEventID() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + var e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + var p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "New events should not be timed.") + let event1UUID = uuid(1) + let event2UUID = uuid(2) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.track(event: "Event 1", timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "The exact same timedEventID is required for timing, regardless of name") + testMixpanel.track(event: "Event 1", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$duration"], "This event should be timed.") + testMixpanel.track(event: "Event 1", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil( + p["$duration"], + "Tracking the same event should require a second call to timeEvent.") + removeDBfile(testMixpanel.apiToken) + } + + func testClearingTimedEventsWithId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let event1UUID = uuid(1) + let event2UUID = uuid(2) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.time(timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 2, "Each call to time() should add an event to timedEvents") + XCTAssertNotNil(testTimedEvents[event1UUID.uuidString], "Keys in timedEvents should be timedEventID's uuidString") + XCTAssertNotNil(testTimedEvents[event2UUID.uuidString], "Keys in timedEvents should be timedEventID's uuidString") + testMixpanel.clearTimedEvent(timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertNil(testTimedEvents[event1UUID.uuidString], "clearTimedEvent should remove key/value pair") + XCTAssertNotNil(testTimedEvents[event2UUID.uuidString], "clearTimedEvent should only remove the correct timedEventID's uuidString") + XCTAssertTrue( + testTimedEvents.count == 1, "clearTimedEvent shoud remove only one key/value pair") + removeDBfile(testMixpanel.apiToken) + } + + func testEventTimingMultipleTimedEventsWithId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let event1UUID = uuid(1) + let event2UUID = uuid(2) + let event3UUID = uuid(3) + let event4UUID = uuid(4) + testMixpanel.time(timedEventID: event1UUID) + testMixpanel.time(timedEventID: event2UUID) + testMixpanel.time(timedEventID: event3UUID) + testMixpanel.time(timedEventID: event4UUID) + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 4, "Each call to time() should add an event to timedEvents") + testMixpanel.track(event: "Recurring Event", timedEventID: event1UUID) + waitForTrackingQueue(testMixpanel) + let event4 = eventQueue(token: testMixpanel.apiToken).last! + let event4Properties = event4["properties"] as! InternalProperties + XCTAssertNotNil(event4Properties["$duration"], "This event should be timed.") + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 3, "tracking and event shoud remove only one key/value pair") + testMixpanel.track(event: "Recurring Event", timedEventID: event2UUID) + waitForTrackingQueue(testMixpanel) + let event5 = eventQueue(token: testMixpanel.apiToken).last! + let event5Properties = event5["properties"] as! InternalProperties + XCTAssertNotNil( + event5Properties["$duration"], "This event should be timed. Event though there was another event with the same name.") + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + testTimedEvents.count == 2, "tracking and event shoud remove only one key/value pair") + testMixpanel.clearTimedEvents() + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken) + XCTAssertTrue(testTimedEvents.count == 0, + "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel.apiToken) + } func testReadWriteLock() { var array = [Int]() diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift index 67f666be..27d11200 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift @@ -271,6 +271,21 @@ class MixpanelOptOutTests: MixpanelBaseTests { "When opted out, this event should not be timed.") removeDBfile(testMixpanel.apiToken) } + + func testOptOutWilSkipTimeEventWithId() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + let eventUUID = uuid(1) + testMixpanel.time(timedEventID: eventUUID) + testMixpanel.track(event: "400 Meters", timedEventID: eventUUID) + waitForTrackingQueue(testMixpanel) + XCTAssertNil( + eventQueue(token: testMixpanel.apiToken).last, + "When opted out, this event should not be timed.") + removeDBfile(testMixpanel.apiToken) + } func testOptOutWillSkipFlushPeople() { let testMixpanel = Mixpanel.initialize( diff --git a/Sources/MixpanelInstance.swift b/Sources/MixpanelInstance.swift index 2f1b993e..9715c30c 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -48,6 +48,8 @@ public protocol MixpanelDelegate: AnyObject { } public typealias Properties = [String: MixpanelType] +typealias TimedEventID = String +typealias TimedEvents = [TimedEventID: TimeInterval] typealias InternalProperties = [String: Any] typealias Queue = [InternalProperties] @@ -251,7 +253,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) @@ -970,7 +972,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 @@ -1167,16 +1169,36 @@ extension MixpanelInstance { - parameter properties: properties dictionary */ public func track(event: String?, properties: Properties? = nil) { + track(eventName: event, timedEventID: 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 timedEventID: the `UUID` used to link timed events previously timed using `time(timedEventID)` + - parameter properties: properties dictionary + */ + public func track(event: String?, timedEventID: UUID, properties: Properties? = nil) { + track(eventName: event, timedEventID: timedEventID, properties: properties) + } + + func track(eventName: String?, timedEventID: UUID?, properties: Properties? = nil) { let epochInterval = Date().timeIntervalSince1970 - - trackingQueue.async { [weak self, event, properties, epochInterval] in + trackingQueue.async { [weak self, eventName, timedEventID, properties, epochInterval] in guard let self else { return } if self.hasOptedOutTracking() { return } - var shadowTimedEvents = InternalProperties() + var shadowTimedEvents = TimedEvents() var shadowSuperProperties = InternalProperties() self.readWriteLock.read { @@ -1192,7 +1214,8 @@ extension MixpanelInstance { alias: nil, hadPersistedDistinctId: self.hadPersistedDistinctId) let timedEventsSnapshot = self.trackInstance.track( - event: event, + event: eventName, + timedEventID: timedEventID, properties: properties, timedEvents: shadowTimedEvents, superProperties: shadowSuperProperties, @@ -1323,11 +1346,54 @@ 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 same `timedEventID:UUID` 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. + + This method allows you to link a `time(timedEventID:)` call with a corresponding + `track(event:,timedEventID:)` call which is needed when you have multiple + events with the same name that you want to distinguish from one another by using a distinct Id. + + This method also allows you to name the event at the time of tracking rather than needing to + know the name of the event when you start timing the event. This is useful for events whose name + may be dependent on the result of a call and whose outcome at the start is unknown. + + - precondition: + // Create a uuid to track this event + let eventId = UUID("0000-...-0000") + // begin timing the image upload: + mixpanelInstance.time(timedEventID:eventId) + // upload the image: + self.uploadImageWithSuccessHandler() { _ in + // track the event in the success case + mixpanelInstance.track("Image Upload Succeeded", timedEventID: eventId) + } failed: { + // track the event in the failure case + mixpanelInstance.track("Image Upload Failed", timedEventID: eventId) + } + + - parameter event: the event name to be timed + + */ + public func time(timedEventID: UUID) { + time(eventID: timedEventID.uuidString) + } + + func time(eventID: TimedEventID) { 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) + eventID: eventID, timedEvents: self.timedEvents, startTime: startTime) self.readWriteLock.write { self.timedEvents = timedEvents } @@ -1340,13 +1406,26 @@ 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() + public func eventElapsedTime(event: String) -> TimeInterval { + eventElapsedTime(eventID: event) + } + + /** + Retrieves the time elapsed for the timed event since `time(timedEventId:)` was called. + + - parameter timedEventID: the UUID of the event to be tracked that was passed to time(timedEventId:) + */ + public func eventElapsedTime(timedEventID: UUID) -> TimeInterval { + eventElapsedTime(eventID: timedEventID.uuidString) + } + + func eventElapsedTime(eventID: TimedEventID) -> TimeInterval { + 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 @@ -1359,10 +1438,10 @@ 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) + timedEvents: TimedEvents(), instanceName: self.name) } } @@ -1372,11 +1451,24 @@ 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 timed event. + + - parameter timedEventID: the UUID of the event to clear the timer for + */ + public func clearTimedEvent(timedEventID: UUID) { + clearTimedEvent(eventID: timedEventID.uuidString) + } + + func clearTimedEvent(eventID: TimedEventID) { + trackingQueue.async { [weak self, eventID] in guard let self = self else { return } let updatedTimedEvents = self.trackInstance.clearTimedEvent( - event: event, timedEvents: self.timedEvents) + eventID: eventID, timedEvents: self.timedEvents) MixpanelPersistence.saveTimedEvents(timedEvents: updatedTimedEvents, instanceName: self.name) } } @@ -1639,7 +1731,7 @@ extension MixpanelInstance { self.hadPersistedDistinctId = true self.superProperties = InternalProperties() MixpanelPersistence.saveTimedEvents( - timedEvents: InternalProperties(), instanceName: self.name) + timedEvents: TimedEvents(), instanceName: self.name) } self.archive() self.readWriteLock.write { diff --git a/Sources/MixpanelPersistence.swift b/Sources/MixpanelPersistence.swift index cf15579d..0de6e646 100644 --- a/Sources/MixpanelPersistence.swift +++ b/Sources/MixpanelPersistence.swift @@ -138,7 +138,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 } @@ -153,24 +153,24 @@ 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() + ofClasses: archivedClasses, from: timedEventsData) as? TimedEvents + ?? TimedEvents() } catch { MixpanelLogger.warn(message: "Failed to unarchive timed events") - return InternalProperties() + return TimedEvents() } } @@ -369,7 +369,7 @@ class MixpanelPersistence { peopleQueue: Queue, groupsQueue: Queue, superProperties: InternalProperties, - timedEvents: InternalProperties, + timedEvents: TimedEvents, distinctId: String, anonymousId: String?, userId: String?, @@ -479,7 +479,7 @@ class MixpanelPersistence { private func unarchiveProperties() -> ( InternalProperties, - InternalProperties, + TimedEvents, String, String?, String?, @@ -492,7 +492,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 046cfb8d..60315c3d 100644 --- a/Sources/Track.swift +++ b/Sources/Track.swift @@ -35,25 +35,27 @@ class Track { func track( event: String?, + timedEventID: UUID?, properties: Properties? = nil, - timedEvents: InternalProperties, + timedEvents: TimedEvents, superProperties: InternalProperties, mixpanelIdentity: MixpanelIdentity, epochInterval: Double - ) -> InternalProperties { - var ev = "mp_event" + ) -> TimedEvents { + var eventName = "mp_event" if let event = event { - ev = 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 eventID = timedEventID?.uuidString ?? eventName assertPropertyTypes(properties) let epochMilliseconds = round(epochInterval * 1000) - let eventStartTime = timedEvents[ev] as? Double + let eventStartTime = timedEvents[eventID] var p = InternalProperties() AutomaticProperties.automaticPropertiesLock.read { p += AutomaticProperties.properties @@ -62,7 +64,8 @@ class Track { p["time"] = epochMilliseconds var shadowTimedEvents = timedEvents if let eventStartTime = eventStartTime { - shadowTimedEvents.removeValue(forKey: ev) + print("shadowTimedEvents before removing \(eventID): \(shadowTimedEvents as AnyObject)") + shadowTimedEvents.removeValue(forKey: eventID) p["$duration"] = Double(String(format: "%.3f", epochInterval - eventStartTime)) } p["distinct_id"] = mixpanelIdentity.distinctID @@ -81,7 +84,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) @@ -147,34 +150,35 @@ 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) + print("updatedTimedEvents before removing \(eventID): \(updatedTimedEvents as AnyObject)") + updatedTimedEvents.removeValue(forKey: eventID) return updatedTimedEvents } }